blob: 7a39e201143be7befeaa1c2cfcc3d4037c5de86f [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
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
41import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060042import uuid
43
Clark Boylanb640e052014-04-03 16:41:46 -070044
45import git
46import gear
47import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080048import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080049import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060050import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070051import statsd
52import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blaire511d2f2016-12-08 15:22:26 -080058import zuul.driver.gerrit.gerritsource as gerritsource
59import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070069import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080070import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070071
72FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
73 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080074
75KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070076
Clark Boylanb640e052014-04-03 16:41:46 -070077
78def repack_repo(path):
79 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
80 output = subprocess.Popen(cmd, close_fds=True,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE)
83 out = output.communicate()
84 if output.returncode:
85 raise Exception("git repack returned %d" % output.returncode)
86 return out
87
88
89def random_sha1():
90 return hashlib.sha1(str(random.random())).hexdigest()
91
92
James E. Blaira190f3b2015-01-05 14:56:54 -080093def iterate_timeout(max_seconds, purpose):
94 start = time.time()
95 count = 0
96 while (time.time() < start + max_seconds):
97 count += 1
98 yield count
99 time.sleep(0)
100 raise Exception("Timeout waiting for %s" % purpose)
101
102
Jesse Keating436a5452017-04-20 11:48:41 -0700103def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700104 """Specify a layout file for use by a test method.
105
106 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700107 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700108
109 Some tests require only a very simple configuration. For those,
110 establishing a complete config directory hierachy is too much
111 work. In those cases, you can add a simple zuul.yaml file to the
112 test fixtures directory (in fixtures/layouts/foo.yaml) and use
113 this decorator to indicate the test method should use that rather
114 than the tenant config file specified by the test class.
115
116 The decorator will cause that layout file to be added to a
117 config-project called "common-config" and each "project" instance
118 referenced in the layout file will have a git repo automatically
119 initialized.
120 """
121
122 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700123 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700124 return test
125 return decorator
126
127
Clark Boylanb640e052014-04-03 16:41:46 -0700128class ChangeReference(git.Reference):
129 _common_path_default = "refs/changes"
130 _points_to_commits_only = True
131
132
133class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700134 categories = {'approved': ('Approved', -1, 1),
135 'code-review': ('Code-Review', -2, 2),
136 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700137
138 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700139 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700140 self.gerrit = gerrit
141 self.reported = 0
142 self.queried = 0
143 self.patchsets = []
144 self.number = number
145 self.project = project
146 self.branch = branch
147 self.subject = subject
148 self.latest_patchset = 0
149 self.depends_on_change = None
150 self.needed_by_changes = []
151 self.fail_merge = False
152 self.messages = []
153 self.data = {
154 'branch': branch,
155 'comments': [],
156 'commitMessage': subject,
157 'createdOn': time.time(),
158 'id': 'I' + random_sha1(),
159 'lastUpdated': time.time(),
160 'number': str(number),
161 'open': status == 'NEW',
162 'owner': {'email': 'user@example.com',
163 'name': 'User Name',
164 'username': 'username'},
165 'patchSets': self.patchsets,
166 'project': project,
167 'status': status,
168 'subject': subject,
169 'submitRecords': [],
170 'url': 'https://hostname/%s' % number}
171
172 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700173 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700174 self.data['submitRecords'] = self.getSubmitRecords()
175 self.open = status == 'NEW'
176
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700178 path = os.path.join(self.upstream_root, self.project)
179 repo = git.Repo(path)
180 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
181 self.latest_patchset),
182 'refs/tags/init')
183 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700184 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700185 repo.git.clean('-x', '-f', '-d')
186
187 path = os.path.join(self.upstream_root, self.project)
188 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700189 for fn, content in files.items():
190 fn = os.path.join(path, fn)
191 with open(fn, 'w') as f:
192 f.write(content)
193 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700194 else:
195 for fni in range(100):
196 fn = os.path.join(path, str(fni))
197 f = open(fn, 'w')
198 for ci in range(4096):
199 f.write(random.choice(string.printable))
200 f.close()
201 repo.index.add([fn])
202
203 r = repo.index.commit(msg)
204 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700205 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700206 repo.git.clean('-x', '-f', '-d')
207 repo.heads['master'].checkout()
208 return r
209
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700210 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700211 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700212 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700213 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 data = ("test %s %s %s\n" %
215 (self.branch, self.number, self.latest_patchset))
216 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700217 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700219 ps_files = [{'file': '/COMMIT_MSG',
220 'type': 'ADDED'},
221 {'file': 'README',
222 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700223 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700224 ps_files.append({'file': f, 'type': 'ADDED'})
225 d = {'approvals': [],
226 'createdOn': time.time(),
227 'files': ps_files,
228 'number': str(self.latest_patchset),
229 'ref': 'refs/changes/1/%s/%s' % (self.number,
230 self.latest_patchset),
231 'revision': c.hexsha,
232 'uploader': {'email': 'user@example.com',
233 'name': 'User name',
234 'username': 'user'}}
235 self.data['currentPatchSet'] = d
236 self.patchsets.append(d)
237 self.data['submitRecords'] = self.getSubmitRecords()
238
239 def getPatchsetCreatedEvent(self, patchset):
240 event = {"type": "patchset-created",
241 "change": {"project": self.project,
242 "branch": self.branch,
243 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
244 "number": str(self.number),
245 "subject": self.subject,
246 "owner": {"name": "User Name"},
247 "url": "https://hostname/3"},
248 "patchSet": self.patchsets[patchset - 1],
249 "uploader": {"name": "User Name"}}
250 return event
251
252 def getChangeRestoredEvent(self):
253 event = {"type": "change-restored",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100262 "patchSet": self.patchsets[-1],
263 "reason": ""}
264 return event
265
266 def getChangeAbandonedEvent(self):
267 event = {"type": "change-abandoned",
268 "change": {"project": self.project,
269 "branch": self.branch,
270 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
271 "number": str(self.number),
272 "subject": self.subject,
273 "owner": {"name": "User Name"},
274 "url": "https://hostname/3"},
275 "abandoner": {"name": "User Name"},
276 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700277 "reason": ""}
278 return event
279
280 def getChangeCommentEvent(self, patchset):
281 event = {"type": "comment-added",
282 "change": {"project": self.project,
283 "branch": self.branch,
284 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
285 "number": str(self.number),
286 "subject": self.subject,
287 "owner": {"name": "User Name"},
288 "url": "https://hostname/3"},
289 "patchSet": self.patchsets[patchset - 1],
290 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700291 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700292 "description": "Code-Review",
293 "value": "0"}],
294 "comment": "This is a comment"}
295 return event
296
James E. Blairc2a5ed72017-02-20 14:12:01 -0500297 def getChangeMergedEvent(self):
298 event = {"submitter": {"name": "Jenkins",
299 "username": "jenkins"},
300 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
301 "patchSet": self.patchsets[-1],
302 "change": self.data,
303 "type": "change-merged",
304 "eventCreatedOn": 1487613810}
305 return event
306
James E. Blair8cce42e2016-10-18 08:18:36 -0700307 def getRefUpdatedEvent(self):
308 path = os.path.join(self.upstream_root, self.project)
309 repo = git.Repo(path)
310 oldrev = repo.heads[self.branch].commit.hexsha
311
312 event = {
313 "type": "ref-updated",
314 "submitter": {
315 "name": "User Name",
316 },
317 "refUpdate": {
318 "oldRev": oldrev,
319 "newRev": self.patchsets[-1]['revision'],
320 "refName": self.branch,
321 "project": self.project,
322 }
323 }
324 return event
325
Joshua Hesketh642824b2014-07-01 17:54:59 +1000326 def addApproval(self, category, value, username='reviewer_john',
327 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700328 if not granted_on:
329 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000330 approval = {
331 'description': self.categories[category][0],
332 'type': category,
333 'value': str(value),
334 'by': {
335 'username': username,
336 'email': username + '@example.com',
337 },
338 'grantedOn': int(granted_on)
339 }
Clark Boylanb640e052014-04-03 16:41:46 -0700340 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
341 if x['by']['username'] == username and x['type'] == category:
342 del self.patchsets[-1]['approvals'][i]
343 self.patchsets[-1]['approvals'].append(approval)
344 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000345 'author': {'email': 'author@example.com',
346 'name': 'Patchset Author',
347 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700348 'change': {'branch': self.branch,
349 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
350 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000351 'owner': {'email': 'owner@example.com',
352 'name': 'Change Owner',
353 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700354 'project': self.project,
355 'subject': self.subject,
356 'topic': 'master',
357 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700359 'patchSet': self.patchsets[-1],
360 'type': 'comment-added'}
361 self.data['submitRecords'] = self.getSubmitRecords()
362 return json.loads(json.dumps(event))
363
364 def getSubmitRecords(self):
365 status = {}
366 for cat in self.categories.keys():
367 status[cat] = 0
368
369 for a in self.patchsets[-1]['approvals']:
370 cur = status[a['type']]
371 cat_min, cat_max = self.categories[a['type']][1:]
372 new = int(a['value'])
373 if new == cat_min:
374 cur = new
375 elif abs(new) > abs(cur):
376 cur = new
377 status[a['type']] = cur
378
379 labels = []
380 ok = True
381 for typ, cat in self.categories.items():
382 cur = status[typ]
383 cat_min, cat_max = cat[1:]
384 if cur == cat_min:
385 value = 'REJECT'
386 ok = False
387 elif cur == cat_max:
388 value = 'OK'
389 else:
390 value = 'NEED'
391 ok = False
392 labels.append({'label': cat[0], 'status': value})
393 if ok:
394 return [{'status': 'OK'}]
395 return [{'status': 'NOT_READY',
396 'labels': labels}]
397
398 def setDependsOn(self, other, patchset):
399 self.depends_on_change = other
400 d = {'id': other.data['id'],
401 'number': other.data['number'],
402 'ref': other.patchsets[patchset - 1]['ref']
403 }
404 self.data['dependsOn'] = [d]
405
406 other.needed_by_changes.append(self)
407 needed = other.data.get('neededBy', [])
408 d = {'id': self.data['id'],
409 'number': self.data['number'],
410 'ref': self.patchsets[patchset - 1]['ref'],
411 'revision': self.patchsets[patchset - 1]['revision']
412 }
413 needed.append(d)
414 other.data['neededBy'] = needed
415
416 def query(self):
417 self.queried += 1
418 d = self.data.get('dependsOn')
419 if d:
420 d = d[0]
421 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
422 d['isCurrentPatchSet'] = True
423 else:
424 d['isCurrentPatchSet'] = False
425 return json.loads(json.dumps(self.data))
426
427 def setMerged(self):
428 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000429 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700430 return
431 if self.fail_merge:
432 return
433 self.data['status'] = 'MERGED'
434 self.open = False
435
436 path = os.path.join(self.upstream_root, self.project)
437 repo = git.Repo(path)
438 repo.heads[self.branch].commit = \
439 repo.commit(self.patchsets[-1]['revision'])
440
441 def setReported(self):
442 self.reported += 1
443
444
James E. Blaire511d2f2016-12-08 15:22:26 -0800445class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700446 """A Fake Gerrit connection for use in tests.
447
448 This subclasses
449 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
450 ability for tests to add changes to the fake Gerrit it represents.
451 """
452
Joshua Hesketh352264b2015-08-11 23:42:08 +1000453 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700454
James E. Blaire511d2f2016-12-08 15:22:26 -0800455 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700456 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800457 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000458 connection_config)
459
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700461 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
462 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000463 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700464 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200465 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700466
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700467 def addFakeChange(self, project, branch, subject, status='NEW',
468 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700469 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700470 self.change_number += 1
471 c = FakeChange(self, self.change_number, project, branch, subject,
472 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700473 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.changes[self.change_number] = c
475 return c
476
Clark Boylanb640e052014-04-03 16:41:46 -0700477 def review(self, project, changeid, message, action):
478 number, ps = changeid.split(',')
479 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000480
481 # Add the approval back onto the change (ie simulate what gerrit would
482 # do).
483 # Usually when zuul leaves a review it'll create a feedback loop where
484 # zuul's review enters another gerrit event (which is then picked up by
485 # zuul). However, we can't mimic this behaviour (by adding this
486 # approval event into the queue) as it stops jobs from checking what
487 # happens before this event is triggered. If a job needs to see what
488 # happens they can add their own verified event into the queue.
489 # Nevertheless, we can update change with the new review in gerrit.
490
James E. Blair8b5408c2016-08-08 15:37:46 -0700491 for cat in action.keys():
492 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000493 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000496 if 'label' in action:
497 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000498 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000501
Clark Boylanb640e052014-04-03 16:41:46 -0700502 if 'submit' in action:
503 change.setMerged()
504 if message:
505 change.setReported()
506
507 def query(self, number):
508 change = self.changes.get(int(number))
509 if change:
510 return change.query()
511 return {}
512
James E. Blairc494d542014-08-06 09:23:52 -0700513 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700514 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700515 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800516 if query.startswith('change:'):
517 # Query a specific changeid
518 changeid = query[len('change:'):]
519 l = [change.query() for change in self.changes.values()
520 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700521 elif query.startswith('message:'):
522 # Query the content of a commit message
523 msg = query[len('message:'):].strip()
524 l = [change.query() for change in self.changes.values()
525 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800526 else:
527 # Query all open changes
528 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700529 return l
James E. Blairc494d542014-08-06 09:23:52 -0700530
Joshua Hesketh352264b2015-08-11 23:42:08 +1000531 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700532 pass
533
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def getGitUrl(self, project):
535 return os.path.join(self.upstream_root, project.name)
536
Clark Boylanb640e052014-04-03 16:41:46 -0700537
538class BuildHistory(object):
539 def __init__(self, **kw):
540 self.__dict__.update(kw)
541
542 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700543 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
544 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700545
546
547class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200548 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700549 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700550 self.url = url
551
552 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700553 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700554 path = res.path
555 project = '/'.join(path.split('/')[2:-2])
556 ret = '001e# service=git-upload-pack\n'
557 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
558 'multi_ack thin-pack side-band side-band-64k ofs-delta '
559 'shallow no-progress include-tag multi_ack_detailed no-done\n')
560 path = os.path.join(self.upstream_root, project)
561 repo = git.Repo(path)
562 for ref in repo.refs:
563 r = ref.object.hexsha + ' ' + ref.path + '\n'
564 ret += '%04x%s' % (len(r) + 4, r)
565 ret += '0000'
566 return ret
567
568
Clark Boylanb640e052014-04-03 16:41:46 -0700569class FakeStatsd(threading.Thread):
570 def __init__(self):
571 threading.Thread.__init__(self)
572 self.daemon = True
573 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
574 self.sock.bind(('', 0))
575 self.port = self.sock.getsockname()[1]
576 self.wake_read, self.wake_write = os.pipe()
577 self.stats = []
578
579 def run(self):
580 while True:
581 poll = select.poll()
582 poll.register(self.sock, select.POLLIN)
583 poll.register(self.wake_read, select.POLLIN)
584 ret = poll.poll()
585 for (fd, event) in ret:
586 if fd == self.sock.fileno():
587 data = self.sock.recvfrom(1024)
588 if not data:
589 return
590 self.stats.append(data[0])
591 if fd == self.wake_read:
592 return
593
594 def stop(self):
595 os.write(self.wake_write, '1\n')
596
597
James E. Blaire1767bc2016-08-02 10:00:27 -0700598class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700599 log = logging.getLogger("zuul.test")
600
Paul Belanger174a8272017-03-14 13:20:10 -0400601 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700602 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400603 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700604 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700605 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700606 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700607 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700608 # TODOv3(jeblair): self.node is really "the image of the node
609 # assigned". We should rename it (self.node_image?) if we
610 # keep using it like this, or we may end up exposing more of
611 # the complexity around multi-node jobs here
612 # (self.nodes[0].image?)
613 self.node = None
614 if len(self.parameters.get('nodes')) == 1:
615 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700616 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100617 self.pipeline = self.parameters['ZUUL_PIPELINE']
618 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700619 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700620 self.wait_condition = threading.Condition()
621 self.waiting = False
622 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500623 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700624 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700625 self.changes = None
626 if 'ZUUL_CHANGE_IDS' in self.parameters:
627 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700628
James E. Blair3158e282016-08-19 09:34:11 -0700629 def __repr__(self):
630 waiting = ''
631 if self.waiting:
632 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100633 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
634 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700635
Clark Boylanb640e052014-04-03 16:41:46 -0700636 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700637 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700638 self.wait_condition.acquire()
639 self.wait_condition.notify()
640 self.waiting = False
641 self.log.debug("Build %s released" % self.unique)
642 self.wait_condition.release()
643
644 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700645 """Return whether this build is being held.
646
647 :returns: Whether the build is being held.
648 :rtype: bool
649 """
650
Clark Boylanb640e052014-04-03 16:41:46 -0700651 self.wait_condition.acquire()
652 if self.waiting:
653 ret = True
654 else:
655 ret = False
656 self.wait_condition.release()
657 return ret
658
659 def _wait(self):
660 self.wait_condition.acquire()
661 self.waiting = True
662 self.log.debug("Build %s waiting" % self.unique)
663 self.wait_condition.wait()
664 self.wait_condition.release()
665
666 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700667 self.log.debug('Running build %s' % self.unique)
668
Paul Belanger174a8272017-03-14 13:20:10 -0400669 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700670 self.log.debug('Holding build %s' % self.unique)
671 self._wait()
672 self.log.debug("Build %s continuing" % self.unique)
673
James E. Blair412fba82017-01-26 15:00:50 -0800674 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700675 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800676 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700677 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800678 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500679 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800680 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700681
James E. Blaire1767bc2016-08-02 10:00:27 -0700682 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700683
James E. Blaira5dba232016-08-08 15:53:24 -0700684 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400685 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700686 for change in changes:
687 if self.hasChanges(change):
688 return True
689 return False
690
James E. Blaire7b99a02016-08-05 14:27:34 -0700691 def hasChanges(self, *changes):
692 """Return whether this build has certain changes in its git repos.
693
694 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700695 are expected to be present (in order) in the git repository of
696 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700697
698 :returns: Whether the build has the indicated changes.
699 :rtype: bool
700
701 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800702 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500703 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800704 try:
705 repo = git.Repo(path)
706 except NoSuchPathError as e:
707 self.log.debug('%s' % e)
708 return False
709 ref = self.parameters['ZUUL_REF']
710 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
711 commit_message = '%s-1' % change.subject
712 self.log.debug("Checking if build %s has changes; commit_message "
713 "%s; repo_messages %s" % (self, commit_message,
714 repo_messages))
715 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700716 self.log.debug(" messages do not match")
717 return False
718 self.log.debug(" OK")
719 return True
720
Clark Boylanb640e052014-04-03 16:41:46 -0700721
Paul Belanger174a8272017-03-14 13:20:10 -0400722class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
723 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700724
Paul Belanger174a8272017-03-14 13:20:10 -0400725 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700726 they will report that they have started but then pause until
727 released before reporting completion. This attribute may be
728 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400729 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700730 be explicitly released.
731
732 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800733 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700734 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800735 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400736 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700737 self.hold_jobs_in_build = False
738 self.lock = threading.Lock()
739 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700740 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700741 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700742 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800743
James E. Blaira5dba232016-08-08 15:53:24 -0700744 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400745 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700746
747 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700748 :arg Change change: The :py:class:`~tests.base.FakeChange`
749 instance which should cause the job to fail. This job
750 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700751
752 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700753 l = self.fail_tests.get(name, [])
754 l.append(change)
755 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800756
James E. Blair962220f2016-08-03 11:22:38 -0700757 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700758 """Release a held build.
759
760 :arg str regex: A regular expression which, if supplied, will
761 cause only builds with matching names to be released. If
762 not supplied, all builds will be released.
763
764 """
James E. Blair962220f2016-08-03 11:22:38 -0700765 builds = self.running_builds[:]
766 self.log.debug("Releasing build %s (%s)" % (regex,
767 len(self.running_builds)))
768 for build in builds:
769 if not regex or re.match(regex, build.name):
770 self.log.debug("Releasing build %s" %
771 (build.parameters['ZUUL_UUID']))
772 build.release()
773 else:
774 self.log.debug("Not releasing build %s" %
775 (build.parameters['ZUUL_UUID']))
776 self.log.debug("Done releasing builds %s (%s)" %
777 (regex, len(self.running_builds)))
778
Paul Belanger174a8272017-03-14 13:20:10 -0400779 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700780 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700781 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700782 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700783 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800784 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500785 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800786 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100787 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
788 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700789
790 def stopJob(self, job):
791 self.log.debug("handle stop")
792 parameters = json.loads(job.arguments)
793 uuid = parameters['uuid']
794 for build in self.running_builds:
795 if build.unique == uuid:
796 build.aborted = True
797 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400798 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700799
James E. Blaira002b032017-04-18 10:35:48 -0700800 def stop(self):
801 for build in self.running_builds:
802 build.release()
803 super(RecordingExecutorServer, self).stop()
804
Joshua Hesketh50c21782016-10-13 21:34:14 +1100805
Paul Belanger174a8272017-03-14 13:20:10 -0400806class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700807 def doMergeChanges(self, items):
808 # Get a merger in order to update the repos involved in this job.
809 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
810 if not commit: # merge conflict
811 self.recordResult('MERGER_FAILURE')
812 return commit
813
814 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400815 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400816 self.executor_server.lock.acquire()
817 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700818 BuildHistory(name=build.name, result=result, changes=build.changes,
819 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800820 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700821 pipeline=build.parameters['ZUUL_PIPELINE'])
822 )
Paul Belanger174a8272017-03-14 13:20:10 -0400823 self.executor_server.running_builds.remove(build)
824 del self.executor_server.job_builds[self.job.unique]
825 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700826
827 def runPlaybooks(self, args):
828 build = self.executor_server.job_builds[self.job.unique]
829 build.jobdir = self.jobdir
830
831 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
832 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800833 return result
834
Monty Taylore6562aa2017-02-20 07:37:39 -0500835 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400836 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800837
Paul Belanger174a8272017-03-14 13:20:10 -0400838 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600839 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500840 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800841 else:
842 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700843 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800844
James E. Blairad8dca02017-02-21 11:48:32 -0500845 def getHostList(self, args):
846 self.log.debug("hostlist")
847 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400848 for host in hosts:
849 host['host_vars']['ansible_connection'] = 'local'
850
851 hosts.append(dict(
852 name='localhost',
853 host_vars=dict(ansible_connection='local'),
854 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500855 return hosts
856
James E. Blairf5dbd002015-12-23 15:26:17 -0800857
Clark Boylanb640e052014-04-03 16:41:46 -0700858class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700859 """A Gearman server for use in tests.
860
861 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
862 added to the queue but will not be distributed to workers
863 until released. This attribute may be changed at any time and
864 will take effect for subsequently enqueued jobs, but
865 previously held jobs will still need to be explicitly
866 released.
867
868 """
869
Clark Boylanb640e052014-04-03 16:41:46 -0700870 def __init__(self):
871 self.hold_jobs_in_queue = False
872 super(FakeGearmanServer, self).__init__(0)
873
874 def getJobForConnection(self, connection, peek=False):
875 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
876 for job in queue:
877 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400878 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700879 job.waiting = self.hold_jobs_in_queue
880 else:
881 job.waiting = False
882 if job.waiting:
883 continue
884 if job.name in connection.functions:
885 if not peek:
886 queue.remove(job)
887 connection.related_jobs[job.handle] = job
888 job.worker_connection = connection
889 job.running = True
890 return job
891 return None
892
893 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700894 """Release a held job.
895
896 :arg str regex: A regular expression which, if supplied, will
897 cause only jobs with matching names to be released. If
898 not supplied, all jobs will be released.
899 """
Clark Boylanb640e052014-04-03 16:41:46 -0700900 released = False
901 qlen = (len(self.high_queue) + len(self.normal_queue) +
902 len(self.low_queue))
903 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
904 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400905 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700906 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500907 parameters = json.loads(job.arguments)
908 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700909 self.log.debug("releasing queued job %s" %
910 job.unique)
911 job.waiting = False
912 released = True
913 else:
914 self.log.debug("not releasing queued job %s" %
915 job.unique)
916 if released:
917 self.wakeConnections()
918 qlen = (len(self.high_queue) + len(self.normal_queue) +
919 len(self.low_queue))
920 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
921
922
923class FakeSMTP(object):
924 log = logging.getLogger('zuul.FakeSMTP')
925
926 def __init__(self, messages, server, port):
927 self.server = server
928 self.port = port
929 self.messages = messages
930
931 def sendmail(self, from_email, to_email, msg):
932 self.log.info("Sending email from %s, to %s, with msg %s" % (
933 from_email, to_email, msg))
934
935 headers = msg.split('\n\n', 1)[0]
936 body = msg.split('\n\n', 1)[1]
937
938 self.messages.append(dict(
939 from_email=from_email,
940 to_email=to_email,
941 msg=msg,
942 headers=headers,
943 body=body,
944 ))
945
946 return True
947
948 def quit(self):
949 return True
950
951
James E. Blairdce6cea2016-12-20 16:45:32 -0800952class FakeNodepool(object):
953 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800954 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800955
956 log = logging.getLogger("zuul.test.FakeNodepool")
957
958 def __init__(self, host, port, chroot):
959 self.client = kazoo.client.KazooClient(
960 hosts='%s:%s%s' % (host, port, chroot))
961 self.client.start()
962 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800963 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800964 self.thread = threading.Thread(target=self.run)
965 self.thread.daemon = True
966 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800967 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800968
969 def stop(self):
970 self._running = False
971 self.thread.join()
972 self.client.stop()
973 self.client.close()
974
975 def run(self):
976 while self._running:
977 self._run()
978 time.sleep(0.1)
979
980 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800981 if self.paused:
982 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800983 for req in self.getNodeRequests():
984 self.fulfillRequest(req)
985
986 def getNodeRequests(self):
987 try:
988 reqids = self.client.get_children(self.REQUEST_ROOT)
989 except kazoo.exceptions.NoNodeError:
990 return []
991 reqs = []
992 for oid in sorted(reqids):
993 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800994 try:
995 data, stat = self.client.get(path)
996 data = json.loads(data)
997 data['_oid'] = oid
998 reqs.append(data)
999 except kazoo.exceptions.NoNodeError:
1000 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001001 return reqs
1002
James E. Blaire18d4602017-01-05 11:17:28 -08001003 def getNodes(self):
1004 try:
1005 nodeids = self.client.get_children(self.NODE_ROOT)
1006 except kazoo.exceptions.NoNodeError:
1007 return []
1008 nodes = []
1009 for oid in sorted(nodeids):
1010 path = self.NODE_ROOT + '/' + oid
1011 data, stat = self.client.get(path)
1012 data = json.loads(data)
1013 data['_oid'] = oid
1014 try:
1015 lockfiles = self.client.get_children(path + '/lock')
1016 except kazoo.exceptions.NoNodeError:
1017 lockfiles = []
1018 if lockfiles:
1019 data['_lock'] = True
1020 else:
1021 data['_lock'] = False
1022 nodes.append(data)
1023 return nodes
1024
James E. Blaira38c28e2017-01-04 10:33:20 -08001025 def makeNode(self, request_id, node_type):
1026 now = time.time()
1027 path = '/nodepool/nodes/'
1028 data = dict(type=node_type,
1029 provider='test-provider',
1030 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001031 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001032 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001033 public_ipv4='127.0.0.1',
1034 private_ipv4=None,
1035 public_ipv6=None,
1036 allocated_to=request_id,
1037 state='ready',
1038 state_time=now,
1039 created_time=now,
1040 updated_time=now,
1041 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001042 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001043 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001044 data = json.dumps(data)
1045 path = self.client.create(path, data,
1046 makepath=True,
1047 sequence=True)
1048 nodeid = path.split("/")[-1]
1049 return nodeid
1050
James E. Blair6ab79e02017-01-06 10:10:17 -08001051 def addFailRequest(self, request):
1052 self.fail_requests.add(request['_oid'])
1053
James E. Blairdce6cea2016-12-20 16:45:32 -08001054 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001055 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001056 return
1057 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001058 oid = request['_oid']
1059 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001060
James E. Blair6ab79e02017-01-06 10:10:17 -08001061 if oid in self.fail_requests:
1062 request['state'] = 'failed'
1063 else:
1064 request['state'] = 'fulfilled'
1065 nodes = []
1066 for node in request['node_types']:
1067 nodeid = self.makeNode(oid, node)
1068 nodes.append(nodeid)
1069 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001070
James E. Blaira38c28e2017-01-04 10:33:20 -08001071 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001072 path = self.REQUEST_ROOT + '/' + oid
1073 data = json.dumps(request)
1074 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1075 self.client.set(path, data)
1076
1077
James E. Blair498059b2016-12-20 13:50:13 -08001078class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001079 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001080 super(ChrootedKazooFixture, self).__init__()
1081
1082 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1083 if ':' in zk_host:
1084 host, port = zk_host.split(':')
1085 else:
1086 host = zk_host
1087 port = None
1088
1089 self.zookeeper_host = host
1090
1091 if not port:
1092 self.zookeeper_port = 2181
1093 else:
1094 self.zookeeper_port = int(port)
1095
Clark Boylan621ec9a2017-04-07 17:41:33 -07001096 self.test_id = test_id
1097
James E. Blair498059b2016-12-20 13:50:13 -08001098 def _setUp(self):
1099 # Make sure the test chroot paths do not conflict
1100 random_bits = ''.join(random.choice(string.ascii_lowercase +
1101 string.ascii_uppercase)
1102 for x in range(8))
1103
Clark Boylan621ec9a2017-04-07 17:41:33 -07001104 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001105 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1106
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001107 self.addCleanup(self._cleanup)
1108
James E. Blair498059b2016-12-20 13:50:13 -08001109 # Ensure the chroot path exists and clean up any pre-existing znodes.
1110 _tmp_client = kazoo.client.KazooClient(
1111 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1112 _tmp_client.start()
1113
1114 if _tmp_client.exists(self.zookeeper_chroot):
1115 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1116
1117 _tmp_client.ensure_path(self.zookeeper_chroot)
1118 _tmp_client.stop()
1119 _tmp_client.close()
1120
James E. Blair498059b2016-12-20 13:50:13 -08001121 def _cleanup(self):
1122 '''Remove the chroot path.'''
1123 # Need a non-chroot'ed client to remove the chroot path
1124 _tmp_client = kazoo.client.KazooClient(
1125 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1126 _tmp_client.start()
1127 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1128 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001129 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001130
1131
Joshua Heskethd78b4482015-09-14 16:56:34 -06001132class MySQLSchemaFixture(fixtures.Fixture):
1133 def setUp(self):
1134 super(MySQLSchemaFixture, self).setUp()
1135
1136 random_bits = ''.join(random.choice(string.ascii_lowercase +
1137 string.ascii_uppercase)
1138 for x in range(8))
1139 self.name = '%s_%s' % (random_bits, os.getpid())
1140 self.passwd = uuid.uuid4().hex
1141 db = pymysql.connect(host="localhost",
1142 user="openstack_citest",
1143 passwd="openstack_citest",
1144 db="openstack_citest")
1145 cur = db.cursor()
1146 cur.execute("create database %s" % self.name)
1147 cur.execute(
1148 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1149 (self.name, self.name, self.passwd))
1150 cur.execute("flush privileges")
1151
1152 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1153 self.passwd,
1154 self.name)
1155 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1156 self.addCleanup(self.cleanup)
1157
1158 def cleanup(self):
1159 db = pymysql.connect(host="localhost",
1160 user="openstack_citest",
1161 passwd="openstack_citest",
1162 db="openstack_citest")
1163 cur = db.cursor()
1164 cur.execute("drop database %s" % self.name)
1165 cur.execute("drop user '%s'@'localhost'" % self.name)
1166 cur.execute("flush privileges")
1167
1168
Maru Newby3fe5f852015-01-13 04:22:14 +00001169class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001170 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001171 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001172
James E. Blair1c236df2017-02-01 14:07:24 -08001173 def attachLogs(self, *args):
1174 def reader():
1175 self._log_stream.seek(0)
1176 while True:
1177 x = self._log_stream.read(4096)
1178 if not x:
1179 break
1180 yield x.encode('utf8')
1181 content = testtools.content.content_from_reader(
1182 reader,
1183 testtools.content_type.UTF8_TEXT,
1184 False)
1185 self.addDetail('logging', content)
1186
Clark Boylanb640e052014-04-03 16:41:46 -07001187 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001188 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001189 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1190 try:
1191 test_timeout = int(test_timeout)
1192 except ValueError:
1193 # If timeout value is invalid do not set a timeout.
1194 test_timeout = 0
1195 if test_timeout > 0:
1196 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1197
1198 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1199 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1200 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1201 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1202 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1203 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1204 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1205 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1206 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1207 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001208 self._log_stream = StringIO()
1209 self.addOnException(self.attachLogs)
1210 else:
1211 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001212
James E. Blair1c236df2017-02-01 14:07:24 -08001213 handler = logging.StreamHandler(self._log_stream)
1214 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1215 '%(levelname)-8s %(message)s')
1216 handler.setFormatter(formatter)
1217
1218 logger = logging.getLogger()
1219 logger.setLevel(logging.DEBUG)
1220 logger.addHandler(handler)
1221
1222 # NOTE(notmorgan): Extract logging overrides for specific
1223 # libraries from the OS_LOG_DEFAULTS env and create loggers
1224 # for each. This is used to limit the output during test runs
1225 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001226 log_defaults_from_env = os.environ.get(
1227 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001228 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001229
James E. Blairdce6cea2016-12-20 16:45:32 -08001230 if log_defaults_from_env:
1231 for default in log_defaults_from_env.split(','):
1232 try:
1233 name, level_str = default.split('=', 1)
1234 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001235 logger = logging.getLogger(name)
1236 logger.setLevel(level)
1237 logger.addHandler(handler)
1238 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001239 except ValueError:
1240 # NOTE(notmorgan): Invalid format of the log default,
1241 # skip and don't try and apply a logger for the
1242 # specified module
1243 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001244
Maru Newby3fe5f852015-01-13 04:22:14 +00001245
1246class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001247 """A test case with a functioning Zuul.
1248
1249 The following class variables are used during test setup and can
1250 be overidden by subclasses but are effectively read-only once a
1251 test method starts running:
1252
1253 :cvar str config_file: This points to the main zuul config file
1254 within the fixtures directory. Subclasses may override this
1255 to obtain a different behavior.
1256
1257 :cvar str tenant_config_file: This is the tenant config file
1258 (which specifies from what git repos the configuration should
1259 be loaded). It defaults to the value specified in
1260 `config_file` but can be overidden by subclasses to obtain a
1261 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001262 configuration. See also the :py:func:`simple_layout`
1263 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001264
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001265 :cvar bool create_project_keys: Indicates whether Zuul should
1266 auto-generate keys for each project, or whether the test
1267 infrastructure should insert dummy keys to save time during
1268 startup. Defaults to False.
1269
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 The following are instance variables that are useful within test
1271 methods:
1272
1273 :ivar FakeGerritConnection fake_<connection>:
1274 A :py:class:`~tests.base.FakeGerritConnection` will be
1275 instantiated for each connection present in the config file
1276 and stored here. For instance, `fake_gerrit` will hold the
1277 FakeGerritConnection object for a connection named `gerrit`.
1278
1279 :ivar FakeGearmanServer gearman_server: An instance of
1280 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1281 server that all of the Zuul components in this test use to
1282 communicate with each other.
1283
Paul Belanger174a8272017-03-14 13:20:10 -04001284 :ivar RecordingExecutorServer executor_server: An instance of
1285 :py:class:`~tests.base.RecordingExecutorServer` which is the
1286 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001287
1288 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1289 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001290 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001291 list upon completion.
1292
1293 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1294 objects representing completed builds. They are appended to
1295 the list in the order they complete.
1296
1297 """
1298
James E. Blair83005782015-12-11 14:46:03 -08001299 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001300 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001301 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001302
1303 def _startMerger(self):
1304 self.merge_server = zuul.merger.server.MergeServer(self.config,
1305 self.connections)
1306 self.merge_server.start()
1307
Maru Newby3fe5f852015-01-13 04:22:14 +00001308 def setUp(self):
1309 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001310
1311 self.setupZK()
1312
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001313 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001314 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001315 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1316 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001317 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001318 tmp_root = tempfile.mkdtemp(
1319 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001320 self.test_root = os.path.join(tmp_root, "zuul-test")
1321 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001322 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001323 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001324 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001325
1326 if os.path.exists(self.test_root):
1327 shutil.rmtree(self.test_root)
1328 os.makedirs(self.test_root)
1329 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001330 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001331
1332 # Make per test copy of Configuration.
1333 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001334 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001335 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001336 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001337 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001338 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001339 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001340
Clark Boylanb640e052014-04-03 16:41:46 -07001341 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001342 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1343 # see: https://github.com/jsocol/pystatsd/issues/61
1344 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001345 os.environ['STATSD_PORT'] = str(self.statsd.port)
1346 self.statsd.start()
1347 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001348 reload_module(statsd)
1349 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001350
1351 self.gearman_server = FakeGearmanServer()
1352
1353 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001354 self.log.info("Gearman server on port %s" %
1355 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001356
James E. Blaire511d2f2016-12-08 15:22:26 -08001357 gerritsource.GerritSource.replication_timeout = 1.5
1358 gerritsource.GerritSource.replication_retry_interval = 0.5
1359 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001360
Joshua Hesketh352264b2015-08-11 23:42:08 +10001361 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001362
Jan Hruban6b71aff2015-10-22 16:58:08 +02001363 self.event_queues = [
1364 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001365 self.sched.trigger_event_queue,
1366 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001367 ]
1368
James E. Blairfef78942016-03-11 16:28:56 -08001369 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001370 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001371
Clark Boylanb640e052014-04-03 16:41:46 -07001372 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001373 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001374 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001375 return FakeURLOpener(self.upstream_root, *args, **kw)
1376
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001377 old_urlopen = urllib.request.urlopen
1378 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001379
James E. Blair3f876d52016-07-22 13:07:14 -07001380 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001381
Paul Belanger174a8272017-03-14 13:20:10 -04001382 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001383 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001384 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001385 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001386 _test_root=self.test_root,
1387 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001388 self.executor_server.start()
1389 self.history = self.executor_server.build_history
1390 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001391
Paul Belanger174a8272017-03-14 13:20:10 -04001392 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001393 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001394 self.merge_client = zuul.merger.client.MergeClient(
1395 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001396 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001397 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001398 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001399
James E. Blair0d5a36e2017-02-21 10:53:44 -05001400 self.fake_nodepool = FakeNodepool(
1401 self.zk_chroot_fixture.zookeeper_host,
1402 self.zk_chroot_fixture.zookeeper_port,
1403 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001404
Paul Belanger174a8272017-03-14 13:20:10 -04001405 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001406 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001407 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001408 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001409
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001410 self.webapp = zuul.webapp.WebApp(
1411 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001412 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001413
1414 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001415 self.webapp.start()
1416 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001417 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001418 # Cleanups are run in reverse order
1419 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001420 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001421 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001422
James E. Blairb9c0d772017-03-03 14:34:49 -08001423 self.sched.reconfigure(self.config)
1424 self.sched.resume()
1425
James E. Blairfef78942016-03-11 16:28:56 -08001426 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001427 # Set up gerrit related fakes
1428 # Set a changes database so multiple FakeGerrit's can report back to
1429 # a virtual canonical database given by the configured hostname
1430 self.gerrit_changes_dbs = {}
1431
1432 def getGerritConnection(driver, name, config):
1433 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1434 con = FakeGerritConnection(driver, name, config,
1435 changes_db=db,
1436 upstream_root=self.upstream_root)
1437 self.event_queues.append(con.event_queue)
1438 setattr(self, 'fake_' + name, con)
1439 return con
1440
1441 self.useFixture(fixtures.MonkeyPatch(
1442 'zuul.driver.gerrit.GerritDriver.getConnection',
1443 getGerritConnection))
1444
1445 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001446 # TODO(jhesketh): This should come from lib.connections for better
1447 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001448 # Register connections from the config
1449 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001450
Joshua Hesketh352264b2015-08-11 23:42:08 +10001451 def FakeSMTPFactory(*args, **kw):
1452 args = [self.smtp_messages] + list(args)
1453 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001454
Joshua Hesketh352264b2015-08-11 23:42:08 +10001455 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001456
James E. Blaire511d2f2016-12-08 15:22:26 -08001457 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001458 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001459 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001460
James E. Blair83005782015-12-11 14:46:03 -08001461 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001462 # This creates the per-test configuration object. It can be
1463 # overriden by subclasses, but should not need to be since it
1464 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001465 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001466 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001467
1468 if not self.setupSimpleLayout():
1469 if hasattr(self, 'tenant_config_file'):
1470 self.config.set('zuul', 'tenant_config',
1471 self.tenant_config_file)
1472 git_path = os.path.join(
1473 os.path.dirname(
1474 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1475 'git')
1476 if os.path.exists(git_path):
1477 for reponame in os.listdir(git_path):
1478 project = reponame.replace('_', '/')
1479 self.copyDirToRepo(project,
1480 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001481 self.setupAllProjectKeys()
1482
James E. Blair06cc3922017-04-19 10:08:10 -07001483 def setupSimpleLayout(self):
1484 # If the test method has been decorated with a simple_layout,
1485 # use that instead of the class tenant_config_file. Set up a
1486 # single config-project with the specified layout, and
1487 # initialize repos for all of the 'project' entries which
1488 # appear in the layout.
1489 test_name = self.id().split('.')[-1]
1490 test = getattr(self, test_name)
1491 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001492 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001493 else:
1494 return False
1495
James E. Blairb70e55a2017-04-19 12:57:02 -07001496 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001497 path = os.path.join(FIXTURE_DIR, path)
1498 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001499 data = f.read()
1500 layout = yaml.safe_load(data)
1501 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001502 untrusted_projects = []
1503 for item in layout:
1504 if 'project' in item:
1505 name = item['project']['name']
1506 untrusted_projects.append(name)
1507 self.init_repo(name)
1508 self.addCommitToRepo(name, 'initial commit',
1509 files={'README': ''},
1510 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001511 if 'job' in item:
1512 jobname = item['job']['name']
1513 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001514
1515 root = os.path.join(self.test_root, "config")
1516 if not os.path.exists(root):
1517 os.makedirs(root)
1518 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1519 config = [{'tenant':
1520 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001521 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001522 {'config-projects': ['common-config'],
1523 'untrusted-projects': untrusted_projects}}}}]
1524 f.write(yaml.dump(config))
1525 f.close()
1526 self.config.set('zuul', 'tenant_config',
1527 os.path.join(FIXTURE_DIR, f.name))
1528
1529 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001530 self.addCommitToRepo('common-config', 'add content from fixture',
1531 files, branch='master', tag='init')
1532
1533 return True
1534
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001535 def setupAllProjectKeys(self):
1536 if self.create_project_keys:
1537 return
1538
1539 path = self.config.get('zuul', 'tenant_config')
1540 with open(os.path.join(FIXTURE_DIR, path)) as f:
1541 tenant_config = yaml.safe_load(f.read())
1542 for tenant in tenant_config:
1543 sources = tenant['tenant']['source']
1544 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001545 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001546 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001547 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001548 self.setupProjectKeys(source, project)
1549
1550 def setupProjectKeys(self, source, project):
1551 # Make sure we set up an RSA key for the project so that we
1552 # don't spend time generating one:
1553
1554 key_root = os.path.join(self.state_root, 'keys')
1555 if not os.path.isdir(key_root):
1556 os.mkdir(key_root, 0o700)
1557 private_key_file = os.path.join(key_root, source, project + '.pem')
1558 private_key_dir = os.path.dirname(private_key_file)
1559 self.log.debug("Installing test keys for project %s at %s" % (
1560 project, private_key_file))
1561 if not os.path.isdir(private_key_dir):
1562 os.makedirs(private_key_dir)
1563 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1564 with open(private_key_file, 'w') as o:
1565 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001566
James E. Blair498059b2016-12-20 13:50:13 -08001567 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001568 self.zk_chroot_fixture = self.useFixture(
1569 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001570 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001571 self.zk_chroot_fixture.zookeeper_host,
1572 self.zk_chroot_fixture.zookeeper_port,
1573 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001574
James E. Blair96c6bf82016-01-15 16:20:40 -08001575 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001576 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001577
1578 files = {}
1579 for (dirpath, dirnames, filenames) in os.walk(source_path):
1580 for filename in filenames:
1581 test_tree_filepath = os.path.join(dirpath, filename)
1582 common_path = os.path.commonprefix([test_tree_filepath,
1583 source_path])
1584 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1585 with open(test_tree_filepath, 'r') as f:
1586 content = f.read()
1587 files[relative_filepath] = content
1588 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001589 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001590
James E. Blaire18d4602017-01-05 11:17:28 -08001591 def assertNodepoolState(self):
1592 # Make sure that there are no pending requests
1593
1594 requests = self.fake_nodepool.getNodeRequests()
1595 self.assertEqual(len(requests), 0)
1596
1597 nodes = self.fake_nodepool.getNodes()
1598 for node in nodes:
1599 self.assertFalse(node['_lock'], "Node %s is locked" %
1600 (node['_oid'],))
1601
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001602 def assertNoGeneratedKeys(self):
1603 # Make sure that Zuul did not generate any project keys
1604 # (unless it was supposed to).
1605
1606 if self.create_project_keys:
1607 return
1608
1609 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1610 test_key = i.read()
1611
1612 key_root = os.path.join(self.state_root, 'keys')
1613 for root, dirname, files in os.walk(key_root):
1614 for fn in files:
1615 with open(os.path.join(root, fn)) as f:
1616 self.assertEqual(test_key, f.read())
1617
Clark Boylanb640e052014-04-03 16:41:46 -07001618 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001619 self.log.debug("Assert final state")
1620 # Make sure no jobs are running
1621 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001622 # Make sure that git.Repo objects have been garbage collected.
1623 repos = []
1624 gc.collect()
1625 for obj in gc.get_objects():
1626 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001627 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001628 repos.append(obj)
1629 self.assertEqual(len(repos), 0)
1630 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001631 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001632 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001633 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001634 for tenant in self.sched.abide.tenants.values():
1635 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001636 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001637 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001638
1639 def shutdown(self):
1640 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001641 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001642 self.merge_server.stop()
1643 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001644 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001645 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001646 self.sched.stop()
1647 self.sched.join()
1648 self.statsd.stop()
1649 self.statsd.join()
1650 self.webapp.stop()
1651 self.webapp.join()
1652 self.rpc.stop()
1653 self.rpc.join()
1654 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001655 self.fake_nodepool.stop()
1656 self.zk.disconnect()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001657 # we whitelist watchdog threads as they have relatively long delays
1658 # before noticing they should exit, but they should exit on their own.
1659 threads = [t for t in threading.enumerate()
1660 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001661 if len(threads) > 1:
1662 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001663 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001664
James E. Blaira002b032017-04-18 10:35:48 -07001665 def assertCleanShutdown(self):
1666 pass
1667
James E. Blairc4ba97a2017-04-19 16:26:24 -07001668 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001669 parts = project.split('/')
1670 path = os.path.join(self.upstream_root, *parts[:-1])
1671 if not os.path.exists(path):
1672 os.makedirs(path)
1673 path = os.path.join(self.upstream_root, project)
1674 repo = git.Repo.init(path)
1675
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001676 with repo.config_writer() as config_writer:
1677 config_writer.set_value('user', 'email', 'user@example.com')
1678 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001679
Clark Boylanb640e052014-04-03 16:41:46 -07001680 repo.index.commit('initial commit')
1681 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001682 if tag:
1683 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001684
James E. Blair97d902e2014-08-21 13:25:56 -07001685 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001686 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001687 repo.git.clean('-x', '-f', '-d')
1688
James E. Blair97d902e2014-08-21 13:25:56 -07001689 def create_branch(self, project, branch):
1690 path = os.path.join(self.upstream_root, project)
1691 repo = git.Repo.init(path)
1692 fn = os.path.join(path, 'README')
1693
1694 branch_head = repo.create_head(branch)
1695 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001696 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001697 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001698 f.close()
1699 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001700 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001701
James E. Blair97d902e2014-08-21 13:25:56 -07001702 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001703 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001704 repo.git.clean('-x', '-f', '-d')
1705
Sachi King9f16d522016-03-16 12:20:45 +11001706 def create_commit(self, project):
1707 path = os.path.join(self.upstream_root, project)
1708 repo = git.Repo(path)
1709 repo.head.reference = repo.heads['master']
1710 file_name = os.path.join(path, 'README')
1711 with open(file_name, 'a') as f:
1712 f.write('creating fake commit\n')
1713 repo.index.add([file_name])
1714 commit = repo.index.commit('Creating a fake commit')
1715 return commit.hexsha
1716
James E. Blairf4a5f022017-04-18 14:01:10 -07001717 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001718 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001719 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001720 while len(self.builds):
1721 self.release(self.builds[0])
1722 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001723 i += 1
1724 if count is not None and i >= count:
1725 break
James E. Blairb8c16472015-05-05 14:55:26 -07001726
Clark Boylanb640e052014-04-03 16:41:46 -07001727 def release(self, job):
1728 if isinstance(job, FakeBuild):
1729 job.release()
1730 else:
1731 job.waiting = False
1732 self.log.debug("Queued job %s released" % job.unique)
1733 self.gearman_server.wakeConnections()
1734
1735 def getParameter(self, job, name):
1736 if isinstance(job, FakeBuild):
1737 return job.parameters[name]
1738 else:
1739 parameters = json.loads(job.arguments)
1740 return parameters[name]
1741
Clark Boylanb640e052014-04-03 16:41:46 -07001742 def haveAllBuildsReported(self):
1743 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001744 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001745 return False
1746 # Find out if every build that the worker has completed has been
1747 # reported back to Zuul. If it hasn't then that means a Gearman
1748 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001749 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001750 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001751 if not zbuild:
1752 # It has already been reported
1753 continue
1754 # It hasn't been reported yet.
1755 return False
1756 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001757 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001758 if connection.state == 'GRAB_WAIT':
1759 return False
1760 return True
1761
1762 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001763 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001764 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001765 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001766 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001767 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001768 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001769 for j in conn.related_jobs.values():
1770 if j.unique == build.uuid:
1771 client_job = j
1772 break
1773 if not client_job:
1774 self.log.debug("%s is not known to the gearman client" %
1775 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001776 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001777 if not client_job.handle:
1778 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001779 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001780 server_job = self.gearman_server.jobs.get(client_job.handle)
1781 if not server_job:
1782 self.log.debug("%s is not known to the gearman server" %
1783 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001784 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001785 if not hasattr(server_job, 'waiting'):
1786 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001787 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001788 if server_job.waiting:
1789 continue
James E. Blair17302972016-08-10 16:11:42 -07001790 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001791 self.log.debug("%s has not reported start" % build)
1792 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001793 worker_build = self.executor_server.job_builds.get(
1794 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001795 if worker_build:
1796 if worker_build.isWaiting():
1797 continue
1798 else:
1799 self.log.debug("%s is running" % worker_build)
1800 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001801 else:
James E. Blair962220f2016-08-03 11:22:38 -07001802 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001803 return False
James E. Blaira002b032017-04-18 10:35:48 -07001804 for (build_uuid, job_worker) in \
1805 self.executor_server.job_workers.items():
1806 if build_uuid not in seen_builds:
1807 self.log.debug("%s is not finalized" % build_uuid)
1808 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001809 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001810
James E. Blairdce6cea2016-12-20 16:45:32 -08001811 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001812 if self.fake_nodepool.paused:
1813 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001814 if self.sched.nodepool.requests:
1815 return False
1816 return True
1817
Jan Hruban6b71aff2015-10-22 16:58:08 +02001818 def eventQueuesEmpty(self):
1819 for queue in self.event_queues:
1820 yield queue.empty()
1821
1822 def eventQueuesJoin(self):
1823 for queue in self.event_queues:
1824 queue.join()
1825
Clark Boylanb640e052014-04-03 16:41:46 -07001826 def waitUntilSettled(self):
1827 self.log.debug("Waiting until settled...")
1828 start = time.time()
1829 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001830 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001831 self.log.error("Timeout waiting for Zuul to settle")
1832 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001833 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001834 self.log.error(" %s: %s" % (queue, queue.empty()))
1835 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001836 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001837 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001838 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001839 self.log.error("All requests completed: %s" %
1840 (self.areAllNodeRequestsComplete(),))
1841 self.log.error("Merge client jobs: %s" %
1842 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001843 raise Exception("Timeout waiting for Zuul to settle")
1844 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001845
Paul Belanger174a8272017-03-14 13:20:10 -04001846 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001847 # have all build states propogated to zuul?
1848 if self.haveAllBuildsReported():
1849 # Join ensures that the queue is empty _and_ events have been
1850 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001851 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001852 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001853 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001854 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001855 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001856 self.areAllNodeRequestsComplete() and
1857 all(self.eventQueuesEmpty())):
1858 # The queue empty check is placed at the end to
1859 # ensure that if a component adds an event between
1860 # when locked the run handler and checked that the
1861 # components were stable, we don't erroneously
1862 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001863 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001864 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001865 self.log.debug("...settled.")
1866 return
1867 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001868 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001869 self.sched.wake_event.wait(0.1)
1870
1871 def countJobResults(self, jobs, result):
1872 jobs = filter(lambda x: x.result == result, jobs)
1873 return len(jobs)
1874
James E. Blair96c6bf82016-01-15 16:20:40 -08001875 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001876 for job in self.history:
1877 if (job.name == name and
1878 (project is None or
1879 job.parameters['ZUUL_PROJECT'] == project)):
1880 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001881 raise Exception("Unable to find job %s in history" % name)
1882
1883 def assertEmptyQueues(self):
1884 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001885 for tenant in self.sched.abide.tenants.values():
1886 for pipeline in tenant.layout.pipelines.values():
1887 for queue in pipeline.queues:
1888 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001889 print('pipeline %s queue %s contents %s' % (
1890 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001891 self.assertEqual(len(queue.queue), 0,
1892 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001893
1894 def assertReportedStat(self, key, value=None, kind=None):
1895 start = time.time()
1896 while time.time() < (start + 5):
1897 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001898 k, v = stat.split(':')
1899 if key == k:
1900 if value is None and kind is None:
1901 return
1902 elif value:
1903 if value == v:
1904 return
1905 elif kind:
1906 if v.endswith('|' + kind):
1907 return
1908 time.sleep(0.1)
1909
Clark Boylanb640e052014-04-03 16:41:46 -07001910 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001911
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001912 def assertBuilds(self, builds):
1913 """Assert that the running builds are as described.
1914
1915 The list of running builds is examined and must match exactly
1916 the list of builds described by the input.
1917
1918 :arg list builds: A list of dictionaries. Each item in the
1919 list must match the corresponding build in the build
1920 history, and each element of the dictionary must match the
1921 corresponding attribute of the build.
1922
1923 """
James E. Blair3158e282016-08-19 09:34:11 -07001924 try:
1925 self.assertEqual(len(self.builds), len(builds))
1926 for i, d in enumerate(builds):
1927 for k, v in d.items():
1928 self.assertEqual(
1929 getattr(self.builds[i], k), v,
1930 "Element %i in builds does not match" % (i,))
1931 except Exception:
1932 for build in self.builds:
1933 self.log.error("Running build: %s" % build)
1934 else:
1935 self.log.error("No running builds")
1936 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001937
James E. Blairb536ecc2016-08-31 10:11:42 -07001938 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001939 """Assert that the completed builds are as described.
1940
1941 The list of completed builds is examined and must match
1942 exactly the list of builds described by the input.
1943
1944 :arg list history: A list of dictionaries. Each item in the
1945 list must match the corresponding build in the build
1946 history, and each element of the dictionary must match the
1947 corresponding attribute of the build.
1948
James E. Blairb536ecc2016-08-31 10:11:42 -07001949 :arg bool ordered: If true, the history must match the order
1950 supplied, if false, the builds are permitted to have
1951 arrived in any order.
1952
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001953 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001954 def matches(history_item, item):
1955 for k, v in item.items():
1956 if getattr(history_item, k) != v:
1957 return False
1958 return True
James E. Blair3158e282016-08-19 09:34:11 -07001959 try:
1960 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001961 if ordered:
1962 for i, d in enumerate(history):
1963 if not matches(self.history[i], d):
1964 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001965 "Element %i in history does not match %s" %
1966 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001967 else:
1968 unseen = self.history[:]
1969 for i, d in enumerate(history):
1970 found = False
1971 for unseen_item in unseen:
1972 if matches(unseen_item, d):
1973 found = True
1974 unseen.remove(unseen_item)
1975 break
1976 if not found:
1977 raise Exception("No match found for element %i "
1978 "in history" % (i,))
1979 if unseen:
1980 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001981 except Exception:
1982 for build in self.history:
1983 self.log.error("Completed build: %s" % build)
1984 else:
1985 self.log.error("No completed builds")
1986 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001987
James E. Blair6ac368c2016-12-22 18:07:20 -08001988 def printHistory(self):
1989 """Log the build history.
1990
1991 This can be useful during tests to summarize what jobs have
1992 completed.
1993
1994 """
1995 self.log.debug("Build history:")
1996 for build in self.history:
1997 self.log.debug(build)
1998
James E. Blair59fdbac2015-12-07 17:08:06 -08001999 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002000 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2001
James E. Blair9ea70072017-04-19 16:05:30 -07002002 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002003 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002004 if not os.path.exists(root):
2005 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002006 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2007 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002008- tenant:
2009 name: openstack
2010 source:
2011 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002012 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002013 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002014 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002015 - org/project
2016 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002017 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002018 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002019 self.config.set('zuul', 'tenant_config',
2020 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002021 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002022
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002023 def addCommitToRepo(self, project, message, files,
2024 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002025 path = os.path.join(self.upstream_root, project)
2026 repo = git.Repo(path)
2027 repo.head.reference = branch
2028 zuul.merger.merger.reset_repo_to_head(repo)
2029 for fn, content in files.items():
2030 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002031 try:
2032 os.makedirs(os.path.dirname(fn))
2033 except OSError:
2034 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002035 with open(fn, 'w') as f:
2036 f.write(content)
2037 repo.index.add([fn])
2038 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002039 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002040 repo.heads[branch].commit = commit
2041 repo.head.reference = branch
2042 repo.git.clean('-x', '-f', '-d')
2043 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002044 if tag:
2045 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002046 return before
2047
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002048 def commitConfigUpdate(self, project_name, source_name):
2049 """Commit an update to zuul.yaml
2050
2051 This overwrites the zuul.yaml in the specificed project with
2052 the contents specified.
2053
2054 :arg str project_name: The name of the project containing
2055 zuul.yaml (e.g., common-config)
2056
2057 :arg str source_name: The path to the file (underneath the
2058 test fixture directory) whose contents should be used to
2059 replace zuul.yaml.
2060 """
2061
2062 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002063 files = {}
2064 with open(source_path, 'r') as f:
2065 data = f.read()
2066 layout = yaml.safe_load(data)
2067 files['zuul.yaml'] = data
2068 for item in layout:
2069 if 'job' in item:
2070 jobname = item['job']['name']
2071 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002072 before = self.addCommitToRepo(
2073 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002074 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002075 return before
2076
James E. Blair7fc8daa2016-08-08 15:37:15 -07002077 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002078
James E. Blair7fc8daa2016-08-08 15:37:15 -07002079 """Inject a Fake (Gerrit) event.
2080
2081 This method accepts a JSON-encoded event and simulates Zuul
2082 having received it from Gerrit. It could (and should)
2083 eventually apply to any connection type, but is currently only
2084 used with Gerrit connections. The name of the connection is
2085 used to look up the corresponding server, and the event is
2086 simulated as having been received by all Zuul connections
2087 attached to that server. So if two Gerrit connections in Zuul
2088 are connected to the same Gerrit server, and you invoke this
2089 method specifying the name of one of them, the event will be
2090 received by both.
2091
2092 .. note::
2093
2094 "self.fake_gerrit.addEvent" calls should be migrated to
2095 this method.
2096
2097 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002098 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002099 :arg str event: The JSON-encoded event.
2100
2101 """
2102 specified_conn = self.connections.connections[connection]
2103 for conn in self.connections.connections.values():
2104 if (isinstance(conn, specified_conn.__class__) and
2105 specified_conn.server == conn.server):
2106 conn.addEvent(event)
2107
James E. Blair3f876d52016-07-22 13:07:14 -07002108
2109class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002110 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002111 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002112
Joshua Heskethd78b4482015-09-14 16:56:34 -06002113
2114class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002115 def setup_config(self):
2116 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002117 for section_name in self.config.sections():
2118 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2119 section_name, re.I)
2120 if not con_match:
2121 continue
2122
2123 if self.config.get(section_name, 'driver') == 'sql':
2124 f = MySQLSchemaFixture()
2125 self.useFixture(f)
2126 if (self.config.get(section_name, 'dburi') ==
2127 '$MYSQL_FIXTURE_DBURI$'):
2128 self.config.set(section_name, 'dburi', f.dburi)