blob: d39330ac3ce281c751baf44e1ba8ae012eb1e84f [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
James E. Blair1c236df2017-02-01 14:07:24 -080031from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070032import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
38import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060039import uuid
40
Clark Boylanb640e052014-04-03 16:41:46 -070041
42import git
43import gear
44import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080045import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080046import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060047import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070048import statsd
49import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blaire511d2f2016-12-08 15:22:26 -080055import zuul.driver.gerrit.gerritsource as gerritsource
56import zuul.driver.gerrit.gerritconnection as gerritconnection
Clark Boylanb640e052014-04-03 16:41:46 -070057import zuul.scheduler
58import zuul.webapp
59import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040060import zuul.executor.server
61import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070066import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080067import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070068
69FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
70 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080071
72KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070073
Clark Boylanb640e052014-04-03 16:41:46 -070074
75def repack_repo(path):
76 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
77 output = subprocess.Popen(cmd, close_fds=True,
78 stdout=subprocess.PIPE,
79 stderr=subprocess.PIPE)
80 out = output.communicate()
81 if output.returncode:
82 raise Exception("git repack returned %d" % output.returncode)
83 return out
84
85
86def random_sha1():
87 return hashlib.sha1(str(random.random())).hexdigest()
88
89
James E. Blaira190f3b2015-01-05 14:56:54 -080090def iterate_timeout(max_seconds, purpose):
91 start = time.time()
92 count = 0
93 while (time.time() < start + max_seconds):
94 count += 1
95 yield count
96 time.sleep(0)
97 raise Exception("Timeout waiting for %s" % purpose)
98
99
Jesse Keating436a5452017-04-20 11:48:41 -0700100def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700101 """Specify a layout file for use by a test method.
102
103 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700104 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700105
106 Some tests require only a very simple configuration. For those,
107 establishing a complete config directory hierachy is too much
108 work. In those cases, you can add a simple zuul.yaml file to the
109 test fixtures directory (in fixtures/layouts/foo.yaml) and use
110 this decorator to indicate the test method should use that rather
111 than the tenant config file specified by the test class.
112
113 The decorator will cause that layout file to be added to a
114 config-project called "common-config" and each "project" instance
115 referenced in the layout file will have a git repo automatically
116 initialized.
117 """
118
119 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700120 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700121 return test
122 return decorator
123
124
Clark Boylanb640e052014-04-03 16:41:46 -0700125class ChangeReference(git.Reference):
126 _common_path_default = "refs/changes"
127 _points_to_commits_only = True
128
129
130class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700131 categories = {'approved': ('Approved', -1, 1),
132 'code-review': ('Code-Review', -2, 2),
133 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700134
135 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700136 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700137 self.gerrit = gerrit
138 self.reported = 0
139 self.queried = 0
140 self.patchsets = []
141 self.number = number
142 self.project = project
143 self.branch = branch
144 self.subject = subject
145 self.latest_patchset = 0
146 self.depends_on_change = None
147 self.needed_by_changes = []
148 self.fail_merge = False
149 self.messages = []
150 self.data = {
151 'branch': branch,
152 'comments': [],
153 'commitMessage': subject,
154 'createdOn': time.time(),
155 'id': 'I' + random_sha1(),
156 'lastUpdated': time.time(),
157 'number': str(number),
158 'open': status == 'NEW',
159 'owner': {'email': 'user@example.com',
160 'name': 'User Name',
161 'username': 'username'},
162 'patchSets': self.patchsets,
163 'project': project,
164 'status': status,
165 'subject': subject,
166 'submitRecords': [],
167 'url': 'https://hostname/%s' % number}
168
169 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700170 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700171 self.data['submitRecords'] = self.getSubmitRecords()
172 self.open = status == 'NEW'
173
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700174 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700175 path = os.path.join(self.upstream_root, self.project)
176 repo = git.Repo(path)
177 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
178 self.latest_patchset),
179 'refs/tags/init')
180 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700181 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700182 repo.git.clean('-x', '-f', '-d')
183
184 path = os.path.join(self.upstream_root, self.project)
185 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700186 for fn, content in files.items():
187 fn = os.path.join(path, fn)
188 with open(fn, 'w') as f:
189 f.write(content)
190 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700191 else:
192 for fni in range(100):
193 fn = os.path.join(path, str(fni))
194 f = open(fn, 'w')
195 for ci in range(4096):
196 f.write(random.choice(string.printable))
197 f.close()
198 repo.index.add([fn])
199
200 r = repo.index.commit(msg)
201 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700202 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700203 repo.git.clean('-x', '-f', '-d')
204 repo.heads['master'].checkout()
205 return r
206
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700207 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700208 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700209 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700210 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700211 data = ("test %s %s %s\n" %
212 (self.branch, self.number, self.latest_patchset))
213 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700214 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700216 ps_files = [{'file': '/COMMIT_MSG',
217 'type': 'ADDED'},
218 {'file': 'README',
219 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700220 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700221 ps_files.append({'file': f, 'type': 'ADDED'})
222 d = {'approvals': [],
223 'createdOn': time.time(),
224 'files': ps_files,
225 'number': str(self.latest_patchset),
226 'ref': 'refs/changes/1/%s/%s' % (self.number,
227 self.latest_patchset),
228 'revision': c.hexsha,
229 'uploader': {'email': 'user@example.com',
230 'name': 'User name',
231 'username': 'user'}}
232 self.data['currentPatchSet'] = d
233 self.patchsets.append(d)
234 self.data['submitRecords'] = self.getSubmitRecords()
235
236 def getPatchsetCreatedEvent(self, patchset):
237 event = {"type": "patchset-created",
238 "change": {"project": self.project,
239 "branch": self.branch,
240 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
241 "number": str(self.number),
242 "subject": self.subject,
243 "owner": {"name": "User Name"},
244 "url": "https://hostname/3"},
245 "patchSet": self.patchsets[patchset - 1],
246 "uploader": {"name": "User Name"}}
247 return event
248
249 def getChangeRestoredEvent(self):
250 event = {"type": "change-restored",
251 "change": {"project": self.project,
252 "branch": self.branch,
253 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
254 "number": str(self.number),
255 "subject": self.subject,
256 "owner": {"name": "User Name"},
257 "url": "https://hostname/3"},
258 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100259 "patchSet": self.patchsets[-1],
260 "reason": ""}
261 return event
262
263 def getChangeAbandonedEvent(self):
264 event = {"type": "change-abandoned",
265 "change": {"project": self.project,
266 "branch": self.branch,
267 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
268 "number": str(self.number),
269 "subject": self.subject,
270 "owner": {"name": "User Name"},
271 "url": "https://hostname/3"},
272 "abandoner": {"name": "User Name"},
273 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700274 "reason": ""}
275 return event
276
277 def getChangeCommentEvent(self, patchset):
278 event = {"type": "comment-added",
279 "change": {"project": self.project,
280 "branch": self.branch,
281 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
282 "number": str(self.number),
283 "subject": self.subject,
284 "owner": {"name": "User Name"},
285 "url": "https://hostname/3"},
286 "patchSet": self.patchsets[patchset - 1],
287 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700288 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700289 "description": "Code-Review",
290 "value": "0"}],
291 "comment": "This is a comment"}
292 return event
293
James E. Blairc2a5ed72017-02-20 14:12:01 -0500294 def getChangeMergedEvent(self):
295 event = {"submitter": {"name": "Jenkins",
296 "username": "jenkins"},
297 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
298 "patchSet": self.patchsets[-1],
299 "change": self.data,
300 "type": "change-merged",
301 "eventCreatedOn": 1487613810}
302 return event
303
James E. Blair8cce42e2016-10-18 08:18:36 -0700304 def getRefUpdatedEvent(self):
305 path = os.path.join(self.upstream_root, self.project)
306 repo = git.Repo(path)
307 oldrev = repo.heads[self.branch].commit.hexsha
308
309 event = {
310 "type": "ref-updated",
311 "submitter": {
312 "name": "User Name",
313 },
314 "refUpdate": {
315 "oldRev": oldrev,
316 "newRev": self.patchsets[-1]['revision'],
317 "refName": self.branch,
318 "project": self.project,
319 }
320 }
321 return event
322
Joshua Hesketh642824b2014-07-01 17:54:59 +1000323 def addApproval(self, category, value, username='reviewer_john',
324 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700325 if not granted_on:
326 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000327 approval = {
328 'description': self.categories[category][0],
329 'type': category,
330 'value': str(value),
331 'by': {
332 'username': username,
333 'email': username + '@example.com',
334 },
335 'grantedOn': int(granted_on)
336 }
Clark Boylanb640e052014-04-03 16:41:46 -0700337 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
338 if x['by']['username'] == username and x['type'] == category:
339 del self.patchsets[-1]['approvals'][i]
340 self.patchsets[-1]['approvals'].append(approval)
341 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000342 'author': {'email': 'author@example.com',
343 'name': 'Patchset Author',
344 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700345 'change': {'branch': self.branch,
346 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
347 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'owner': {'email': 'owner@example.com',
349 'name': 'Change Owner',
350 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'project': self.project,
352 'subject': self.subject,
353 'topic': 'master',
354 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700356 'patchSet': self.patchsets[-1],
357 'type': 'comment-added'}
358 self.data['submitRecords'] = self.getSubmitRecords()
359 return json.loads(json.dumps(event))
360
361 def getSubmitRecords(self):
362 status = {}
363 for cat in self.categories.keys():
364 status[cat] = 0
365
366 for a in self.patchsets[-1]['approvals']:
367 cur = status[a['type']]
368 cat_min, cat_max = self.categories[a['type']][1:]
369 new = int(a['value'])
370 if new == cat_min:
371 cur = new
372 elif abs(new) > abs(cur):
373 cur = new
374 status[a['type']] = cur
375
376 labels = []
377 ok = True
378 for typ, cat in self.categories.items():
379 cur = status[typ]
380 cat_min, cat_max = cat[1:]
381 if cur == cat_min:
382 value = 'REJECT'
383 ok = False
384 elif cur == cat_max:
385 value = 'OK'
386 else:
387 value = 'NEED'
388 ok = False
389 labels.append({'label': cat[0], 'status': value})
390 if ok:
391 return [{'status': 'OK'}]
392 return [{'status': 'NOT_READY',
393 'labels': labels}]
394
395 def setDependsOn(self, other, patchset):
396 self.depends_on_change = other
397 d = {'id': other.data['id'],
398 'number': other.data['number'],
399 'ref': other.patchsets[patchset - 1]['ref']
400 }
401 self.data['dependsOn'] = [d]
402
403 other.needed_by_changes.append(self)
404 needed = other.data.get('neededBy', [])
405 d = {'id': self.data['id'],
406 'number': self.data['number'],
407 'ref': self.patchsets[patchset - 1]['ref'],
408 'revision': self.patchsets[patchset - 1]['revision']
409 }
410 needed.append(d)
411 other.data['neededBy'] = needed
412
413 def query(self):
414 self.queried += 1
415 d = self.data.get('dependsOn')
416 if d:
417 d = d[0]
418 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
419 d['isCurrentPatchSet'] = True
420 else:
421 d['isCurrentPatchSet'] = False
422 return json.loads(json.dumps(self.data))
423
424 def setMerged(self):
425 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000426 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700427 return
428 if self.fail_merge:
429 return
430 self.data['status'] = 'MERGED'
431 self.open = False
432
433 path = os.path.join(self.upstream_root, self.project)
434 repo = git.Repo(path)
435 repo.heads[self.branch].commit = \
436 repo.commit(self.patchsets[-1]['revision'])
437
438 def setReported(self):
439 self.reported += 1
440
441
James E. Blaire511d2f2016-12-08 15:22:26 -0800442class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700443 """A Fake Gerrit connection for use in tests.
444
445 This subclasses
446 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
447 ability for tests to add changes to the fake Gerrit it represents.
448 """
449
Joshua Hesketh352264b2015-08-11 23:42:08 +1000450 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700451
James E. Blaire511d2f2016-12-08 15:22:26 -0800452 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700453 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800454 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000455 connection_config)
456
James E. Blair7fc8daa2016-08-08 15:37:15 -0700457 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700458 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
459 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000460 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700461 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200462 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700463
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700464 def addFakeChange(self, project, branch, subject, status='NEW',
465 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700466 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700467 self.change_number += 1
468 c = FakeChange(self, self.change_number, project, branch, subject,
469 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700471 self.changes[self.change_number] = c
472 return c
473
Clark Boylanb640e052014-04-03 16:41:46 -0700474 def review(self, project, changeid, message, action):
475 number, ps = changeid.split(',')
476 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000477
478 # Add the approval back onto the change (ie simulate what gerrit would
479 # do).
480 # Usually when zuul leaves a review it'll create a feedback loop where
481 # zuul's review enters another gerrit event (which is then picked up by
482 # zuul). However, we can't mimic this behaviour (by adding this
483 # approval event into the queue) as it stops jobs from checking what
484 # happens before this event is triggered. If a job needs to see what
485 # happens they can add their own verified event into the queue.
486 # Nevertheless, we can update change with the new review in gerrit.
487
James E. Blair8b5408c2016-08-08 15:37:46 -0700488 for cat in action.keys():
489 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000490 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000491
James E. Blair8b5408c2016-08-08 15:37:46 -0700492 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000493 if 'label' in action:
494 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000495 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000496
Clark Boylanb640e052014-04-03 16:41:46 -0700497 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
Clark Boylanb640e052014-04-03 16:41:46 -0700499 if 'submit' in action:
500 change.setMerged()
501 if message:
502 change.setReported()
503
504 def query(self, number):
505 change = self.changes.get(int(number))
506 if change:
507 return change.query()
508 return {}
509
James E. Blairc494d542014-08-06 09:23:52 -0700510 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700511 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700512 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800513 if query.startswith('change:'):
514 # Query a specific changeid
515 changeid = query[len('change:'):]
516 l = [change.query() for change in self.changes.values()
517 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700518 elif query.startswith('message:'):
519 # Query the content of a commit message
520 msg = query[len('message:'):].strip()
521 l = [change.query() for change in self.changes.values()
522 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800523 else:
524 # Query all open changes
525 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700526 return l
James E. Blairc494d542014-08-06 09:23:52 -0700527
Joshua Hesketh352264b2015-08-11 23:42:08 +1000528 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700529 pass
530
Joshua Hesketh352264b2015-08-11 23:42:08 +1000531 def getGitUrl(self, project):
532 return os.path.join(self.upstream_root, project.name)
533
Clark Boylanb640e052014-04-03 16:41:46 -0700534
535class BuildHistory(object):
536 def __init__(self, **kw):
537 self.__dict__.update(kw)
538
539 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700540 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
541 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700542
543
544class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200545 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700546 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700547 self.url = url
548
549 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700550 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700551 path = res.path
552 project = '/'.join(path.split('/')[2:-2])
553 ret = '001e# service=git-upload-pack\n'
554 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
555 'multi_ack thin-pack side-band side-band-64k ofs-delta '
556 'shallow no-progress include-tag multi_ack_detailed no-done\n')
557 path = os.path.join(self.upstream_root, project)
558 repo = git.Repo(path)
559 for ref in repo.refs:
560 r = ref.object.hexsha + ' ' + ref.path + '\n'
561 ret += '%04x%s' % (len(r) + 4, r)
562 ret += '0000'
563 return ret
564
565
Clark Boylanb640e052014-04-03 16:41:46 -0700566class FakeStatsd(threading.Thread):
567 def __init__(self):
568 threading.Thread.__init__(self)
569 self.daemon = True
570 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
571 self.sock.bind(('', 0))
572 self.port = self.sock.getsockname()[1]
573 self.wake_read, self.wake_write = os.pipe()
574 self.stats = []
575
576 def run(self):
577 while True:
578 poll = select.poll()
579 poll.register(self.sock, select.POLLIN)
580 poll.register(self.wake_read, select.POLLIN)
581 ret = poll.poll()
582 for (fd, event) in ret:
583 if fd == self.sock.fileno():
584 data = self.sock.recvfrom(1024)
585 if not data:
586 return
587 self.stats.append(data[0])
588 if fd == self.wake_read:
589 return
590
591 def stop(self):
592 os.write(self.wake_write, '1\n')
593
594
James E. Blaire1767bc2016-08-02 10:00:27 -0700595class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700596 log = logging.getLogger("zuul.test")
597
Paul Belanger174a8272017-03-14 13:20:10 -0400598 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700599 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400600 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700601 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700602 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700603 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700604 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700605 # TODOv3(jeblair): self.node is really "the image of the node
606 # assigned". We should rename it (self.node_image?) if we
607 # keep using it like this, or we may end up exposing more of
608 # the complexity around multi-node jobs here
609 # (self.nodes[0].image?)
610 self.node = None
611 if len(self.parameters.get('nodes')) == 1:
612 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700613 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100614 self.pipeline = self.parameters['ZUUL_PIPELINE']
615 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700616 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700617 self.wait_condition = threading.Condition()
618 self.waiting = False
619 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500620 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700621 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700622 self.changes = None
623 if 'ZUUL_CHANGE_IDS' in self.parameters:
624 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700625
James E. Blair3158e282016-08-19 09:34:11 -0700626 def __repr__(self):
627 waiting = ''
628 if self.waiting:
629 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100630 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
631 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700632
Clark Boylanb640e052014-04-03 16:41:46 -0700633 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700634 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700635 self.wait_condition.acquire()
636 self.wait_condition.notify()
637 self.waiting = False
638 self.log.debug("Build %s released" % self.unique)
639 self.wait_condition.release()
640
641 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700642 """Return whether this build is being held.
643
644 :returns: Whether the build is being held.
645 :rtype: bool
646 """
647
Clark Boylanb640e052014-04-03 16:41:46 -0700648 self.wait_condition.acquire()
649 if self.waiting:
650 ret = True
651 else:
652 ret = False
653 self.wait_condition.release()
654 return ret
655
656 def _wait(self):
657 self.wait_condition.acquire()
658 self.waiting = True
659 self.log.debug("Build %s waiting" % self.unique)
660 self.wait_condition.wait()
661 self.wait_condition.release()
662
663 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700664 self.log.debug('Running build %s' % self.unique)
665
Paul Belanger174a8272017-03-14 13:20:10 -0400666 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700667 self.log.debug('Holding build %s' % self.unique)
668 self._wait()
669 self.log.debug("Build %s continuing" % self.unique)
670
James E. Blair412fba82017-01-26 15:00:50 -0800671 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700672 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800673 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700674 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800675 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500676 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800677 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700678
James E. Blaire1767bc2016-08-02 10:00:27 -0700679 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700680
James E. Blaira5dba232016-08-08 15:53:24 -0700681 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400682 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700683 for change in changes:
684 if self.hasChanges(change):
685 return True
686 return False
687
James E. Blaire7b99a02016-08-05 14:27:34 -0700688 def hasChanges(self, *changes):
689 """Return whether this build has certain changes in its git repos.
690
691 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700692 are expected to be present (in order) in the git repository of
693 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700694
695 :returns: Whether the build has the indicated changes.
696 :rtype: bool
697
698 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800699 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500700 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800701 try:
702 repo = git.Repo(path)
703 except NoSuchPathError as e:
704 self.log.debug('%s' % e)
705 return False
706 ref = self.parameters['ZUUL_REF']
707 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
708 commit_message = '%s-1' % change.subject
709 self.log.debug("Checking if build %s has changes; commit_message "
710 "%s; repo_messages %s" % (self, commit_message,
711 repo_messages))
712 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700713 self.log.debug(" messages do not match")
714 return False
715 self.log.debug(" OK")
716 return True
717
Clark Boylanb640e052014-04-03 16:41:46 -0700718
Paul Belanger174a8272017-03-14 13:20:10 -0400719class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
720 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700721
Paul Belanger174a8272017-03-14 13:20:10 -0400722 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700723 they will report that they have started but then pause until
724 released before reporting completion. This attribute may be
725 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400726 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700727 be explicitly released.
728
729 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800730 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700731 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800732 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400733 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700734 self.hold_jobs_in_build = False
735 self.lock = threading.Lock()
736 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700737 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700738 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700739 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800740
James E. Blaira5dba232016-08-08 15:53:24 -0700741 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400742 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700743
744 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700745 :arg Change change: The :py:class:`~tests.base.FakeChange`
746 instance which should cause the job to fail. This job
747 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700748
749 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700750 l = self.fail_tests.get(name, [])
751 l.append(change)
752 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800753
James E. Blair962220f2016-08-03 11:22:38 -0700754 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700755 """Release a held build.
756
757 :arg str regex: A regular expression which, if supplied, will
758 cause only builds with matching names to be released. If
759 not supplied, all builds will be released.
760
761 """
James E. Blair962220f2016-08-03 11:22:38 -0700762 builds = self.running_builds[:]
763 self.log.debug("Releasing build %s (%s)" % (regex,
764 len(self.running_builds)))
765 for build in builds:
766 if not regex or re.match(regex, build.name):
767 self.log.debug("Releasing build %s" %
768 (build.parameters['ZUUL_UUID']))
769 build.release()
770 else:
771 self.log.debug("Not releasing build %s" %
772 (build.parameters['ZUUL_UUID']))
773 self.log.debug("Done releasing builds %s (%s)" %
774 (regex, len(self.running_builds)))
775
Paul Belanger174a8272017-03-14 13:20:10 -0400776 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700777 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700778 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700779 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700780 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800781 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500782 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800783 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100784 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
785 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700786
787 def stopJob(self, job):
788 self.log.debug("handle stop")
789 parameters = json.loads(job.arguments)
790 uuid = parameters['uuid']
791 for build in self.running_builds:
792 if build.unique == uuid:
793 build.aborted = True
794 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400795 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700796
James E. Blaira002b032017-04-18 10:35:48 -0700797 def stop(self):
798 for build in self.running_builds:
799 build.release()
800 super(RecordingExecutorServer, self).stop()
801
Joshua Hesketh50c21782016-10-13 21:34:14 +1100802
Paul Belanger174a8272017-03-14 13:20:10 -0400803class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700804 def doMergeChanges(self, items):
805 # Get a merger in order to update the repos involved in this job.
806 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
807 if not commit: # merge conflict
808 self.recordResult('MERGER_FAILURE')
809 return commit
810
811 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400812 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400813 self.executor_server.lock.acquire()
814 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700815 BuildHistory(name=build.name, result=result, changes=build.changes,
816 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800817 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700818 pipeline=build.parameters['ZUUL_PIPELINE'])
819 )
Paul Belanger174a8272017-03-14 13:20:10 -0400820 self.executor_server.running_builds.remove(build)
821 del self.executor_server.job_builds[self.job.unique]
822 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700823
824 def runPlaybooks(self, args):
825 build = self.executor_server.job_builds[self.job.unique]
826 build.jobdir = self.jobdir
827
828 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
829 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800830 return result
831
Monty Taylore6562aa2017-02-20 07:37:39 -0500832 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400833 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800834
Paul Belanger174a8272017-03-14 13:20:10 -0400835 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600836 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500837 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800838 else:
839 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700840 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800841
James E. Blairad8dca02017-02-21 11:48:32 -0500842 def getHostList(self, args):
843 self.log.debug("hostlist")
844 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400845 for host in hosts:
846 host['host_vars']['ansible_connection'] = 'local'
847
848 hosts.append(dict(
849 name='localhost',
850 host_vars=dict(ansible_connection='local'),
851 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500852 return hosts
853
James E. Blairf5dbd002015-12-23 15:26:17 -0800854
Clark Boylanb640e052014-04-03 16:41:46 -0700855class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700856 """A Gearman server for use in tests.
857
858 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
859 added to the queue but will not be distributed to workers
860 until released. This attribute may be changed at any time and
861 will take effect for subsequently enqueued jobs, but
862 previously held jobs will still need to be explicitly
863 released.
864
865 """
866
Clark Boylanb640e052014-04-03 16:41:46 -0700867 def __init__(self):
868 self.hold_jobs_in_queue = False
869 super(FakeGearmanServer, self).__init__(0)
870
871 def getJobForConnection(self, connection, peek=False):
872 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
873 for job in queue:
874 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400875 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700876 job.waiting = self.hold_jobs_in_queue
877 else:
878 job.waiting = False
879 if job.waiting:
880 continue
881 if job.name in connection.functions:
882 if not peek:
883 queue.remove(job)
884 connection.related_jobs[job.handle] = job
885 job.worker_connection = connection
886 job.running = True
887 return job
888 return None
889
890 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700891 """Release a held job.
892
893 :arg str regex: A regular expression which, if supplied, will
894 cause only jobs with matching names to be released. If
895 not supplied, all jobs will be released.
896 """
Clark Boylanb640e052014-04-03 16:41:46 -0700897 released = False
898 qlen = (len(self.high_queue) + len(self.normal_queue) +
899 len(self.low_queue))
900 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
901 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400902 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700903 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500904 parameters = json.loads(job.arguments)
905 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700906 self.log.debug("releasing queued job %s" %
907 job.unique)
908 job.waiting = False
909 released = True
910 else:
911 self.log.debug("not releasing queued job %s" %
912 job.unique)
913 if released:
914 self.wakeConnections()
915 qlen = (len(self.high_queue) + len(self.normal_queue) +
916 len(self.low_queue))
917 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
918
919
920class FakeSMTP(object):
921 log = logging.getLogger('zuul.FakeSMTP')
922
923 def __init__(self, messages, server, port):
924 self.server = server
925 self.port = port
926 self.messages = messages
927
928 def sendmail(self, from_email, to_email, msg):
929 self.log.info("Sending email from %s, to %s, with msg %s" % (
930 from_email, to_email, msg))
931
932 headers = msg.split('\n\n', 1)[0]
933 body = msg.split('\n\n', 1)[1]
934
935 self.messages.append(dict(
936 from_email=from_email,
937 to_email=to_email,
938 msg=msg,
939 headers=headers,
940 body=body,
941 ))
942
943 return True
944
945 def quit(self):
946 return True
947
948
James E. Blairdce6cea2016-12-20 16:45:32 -0800949class FakeNodepool(object):
950 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800951 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800952
953 log = logging.getLogger("zuul.test.FakeNodepool")
954
955 def __init__(self, host, port, chroot):
956 self.client = kazoo.client.KazooClient(
957 hosts='%s:%s%s' % (host, port, chroot))
958 self.client.start()
959 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800960 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800961 self.thread = threading.Thread(target=self.run)
962 self.thread.daemon = True
963 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800964 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800965
966 def stop(self):
967 self._running = False
968 self.thread.join()
969 self.client.stop()
970 self.client.close()
971
972 def run(self):
973 while self._running:
974 self._run()
975 time.sleep(0.1)
976
977 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800978 if self.paused:
979 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800980 for req in self.getNodeRequests():
981 self.fulfillRequest(req)
982
983 def getNodeRequests(self):
984 try:
985 reqids = self.client.get_children(self.REQUEST_ROOT)
986 except kazoo.exceptions.NoNodeError:
987 return []
988 reqs = []
989 for oid in sorted(reqids):
990 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800991 try:
992 data, stat = self.client.get(path)
993 data = json.loads(data)
994 data['_oid'] = oid
995 reqs.append(data)
996 except kazoo.exceptions.NoNodeError:
997 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800998 return reqs
999
James E. Blaire18d4602017-01-05 11:17:28 -08001000 def getNodes(self):
1001 try:
1002 nodeids = self.client.get_children(self.NODE_ROOT)
1003 except kazoo.exceptions.NoNodeError:
1004 return []
1005 nodes = []
1006 for oid in sorted(nodeids):
1007 path = self.NODE_ROOT + '/' + oid
1008 data, stat = self.client.get(path)
1009 data = json.loads(data)
1010 data['_oid'] = oid
1011 try:
1012 lockfiles = self.client.get_children(path + '/lock')
1013 except kazoo.exceptions.NoNodeError:
1014 lockfiles = []
1015 if lockfiles:
1016 data['_lock'] = True
1017 else:
1018 data['_lock'] = False
1019 nodes.append(data)
1020 return nodes
1021
James E. Blaira38c28e2017-01-04 10:33:20 -08001022 def makeNode(self, request_id, node_type):
1023 now = time.time()
1024 path = '/nodepool/nodes/'
1025 data = dict(type=node_type,
1026 provider='test-provider',
1027 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001028 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001029 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001030 public_ipv4='127.0.0.1',
1031 private_ipv4=None,
1032 public_ipv6=None,
1033 allocated_to=request_id,
1034 state='ready',
1035 state_time=now,
1036 created_time=now,
1037 updated_time=now,
1038 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001039 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001040 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001041 data = json.dumps(data)
1042 path = self.client.create(path, data,
1043 makepath=True,
1044 sequence=True)
1045 nodeid = path.split("/")[-1]
1046 return nodeid
1047
James E. Blair6ab79e02017-01-06 10:10:17 -08001048 def addFailRequest(self, request):
1049 self.fail_requests.add(request['_oid'])
1050
James E. Blairdce6cea2016-12-20 16:45:32 -08001051 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001052 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001053 return
1054 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001055 oid = request['_oid']
1056 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001057
James E. Blair6ab79e02017-01-06 10:10:17 -08001058 if oid in self.fail_requests:
1059 request['state'] = 'failed'
1060 else:
1061 request['state'] = 'fulfilled'
1062 nodes = []
1063 for node in request['node_types']:
1064 nodeid = self.makeNode(oid, node)
1065 nodes.append(nodeid)
1066 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001067
James E. Blaira38c28e2017-01-04 10:33:20 -08001068 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001069 path = self.REQUEST_ROOT + '/' + oid
1070 data = json.dumps(request)
1071 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1072 self.client.set(path, data)
1073
1074
James E. Blair498059b2016-12-20 13:50:13 -08001075class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001076 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001077 super(ChrootedKazooFixture, self).__init__()
1078
1079 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1080 if ':' in zk_host:
1081 host, port = zk_host.split(':')
1082 else:
1083 host = zk_host
1084 port = None
1085
1086 self.zookeeper_host = host
1087
1088 if not port:
1089 self.zookeeper_port = 2181
1090 else:
1091 self.zookeeper_port = int(port)
1092
Clark Boylan621ec9a2017-04-07 17:41:33 -07001093 self.test_id = test_id
1094
James E. Blair498059b2016-12-20 13:50:13 -08001095 def _setUp(self):
1096 # Make sure the test chroot paths do not conflict
1097 random_bits = ''.join(random.choice(string.ascii_lowercase +
1098 string.ascii_uppercase)
1099 for x in range(8))
1100
Clark Boylan621ec9a2017-04-07 17:41:33 -07001101 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001102 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1103
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001104 self.addCleanup(self._cleanup)
1105
James E. Blair498059b2016-12-20 13:50:13 -08001106 # Ensure the chroot path exists and clean up any pre-existing znodes.
1107 _tmp_client = kazoo.client.KazooClient(
1108 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1109 _tmp_client.start()
1110
1111 if _tmp_client.exists(self.zookeeper_chroot):
1112 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1113
1114 _tmp_client.ensure_path(self.zookeeper_chroot)
1115 _tmp_client.stop()
1116 _tmp_client.close()
1117
James E. Blair498059b2016-12-20 13:50:13 -08001118 def _cleanup(self):
1119 '''Remove the chroot path.'''
1120 # Need a non-chroot'ed client to remove the chroot path
1121 _tmp_client = kazoo.client.KazooClient(
1122 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1123 _tmp_client.start()
1124 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1125 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001126 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001127
1128
Joshua Heskethd78b4482015-09-14 16:56:34 -06001129class MySQLSchemaFixture(fixtures.Fixture):
1130 def setUp(self):
1131 super(MySQLSchemaFixture, self).setUp()
1132
1133 random_bits = ''.join(random.choice(string.ascii_lowercase +
1134 string.ascii_uppercase)
1135 for x in range(8))
1136 self.name = '%s_%s' % (random_bits, os.getpid())
1137 self.passwd = uuid.uuid4().hex
1138 db = pymysql.connect(host="localhost",
1139 user="openstack_citest",
1140 passwd="openstack_citest",
1141 db="openstack_citest")
1142 cur = db.cursor()
1143 cur.execute("create database %s" % self.name)
1144 cur.execute(
1145 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1146 (self.name, self.name, self.passwd))
1147 cur.execute("flush privileges")
1148
1149 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1150 self.passwd,
1151 self.name)
1152 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1153 self.addCleanup(self.cleanup)
1154
1155 def cleanup(self):
1156 db = pymysql.connect(host="localhost",
1157 user="openstack_citest",
1158 passwd="openstack_citest",
1159 db="openstack_citest")
1160 cur = db.cursor()
1161 cur.execute("drop database %s" % self.name)
1162 cur.execute("drop user '%s'@'localhost'" % self.name)
1163 cur.execute("flush privileges")
1164
1165
Maru Newby3fe5f852015-01-13 04:22:14 +00001166class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001167 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001168 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001169
James E. Blair1c236df2017-02-01 14:07:24 -08001170 def attachLogs(self, *args):
1171 def reader():
1172 self._log_stream.seek(0)
1173 while True:
1174 x = self._log_stream.read(4096)
1175 if not x:
1176 break
1177 yield x.encode('utf8')
1178 content = testtools.content.content_from_reader(
1179 reader,
1180 testtools.content_type.UTF8_TEXT,
1181 False)
1182 self.addDetail('logging', content)
1183
Clark Boylanb640e052014-04-03 16:41:46 -07001184 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001185 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001186 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1187 try:
1188 test_timeout = int(test_timeout)
1189 except ValueError:
1190 # If timeout value is invalid do not set a timeout.
1191 test_timeout = 0
1192 if test_timeout > 0:
1193 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1194
1195 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1196 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1197 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1198 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1199 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1200 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1201 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1202 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1203 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1204 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001205 self._log_stream = StringIO()
1206 self.addOnException(self.attachLogs)
1207 else:
1208 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001209
James E. Blair1c236df2017-02-01 14:07:24 -08001210 handler = logging.StreamHandler(self._log_stream)
1211 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1212 '%(levelname)-8s %(message)s')
1213 handler.setFormatter(formatter)
1214
1215 logger = logging.getLogger()
1216 logger.setLevel(logging.DEBUG)
1217 logger.addHandler(handler)
1218
1219 # NOTE(notmorgan): Extract logging overrides for specific
1220 # libraries from the OS_LOG_DEFAULTS env and create loggers
1221 # for each. This is used to limit the output during test runs
1222 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001223 log_defaults_from_env = os.environ.get(
1224 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001225 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001226
James E. Blairdce6cea2016-12-20 16:45:32 -08001227 if log_defaults_from_env:
1228 for default in log_defaults_from_env.split(','):
1229 try:
1230 name, level_str = default.split('=', 1)
1231 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001232 logger = logging.getLogger(name)
1233 logger.setLevel(level)
1234 logger.addHandler(handler)
1235 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001236 except ValueError:
1237 # NOTE(notmorgan): Invalid format of the log default,
1238 # skip and don't try and apply a logger for the
1239 # specified module
1240 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001241
Maru Newby3fe5f852015-01-13 04:22:14 +00001242
1243class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001244 """A test case with a functioning Zuul.
1245
1246 The following class variables are used during test setup and can
1247 be overidden by subclasses but are effectively read-only once a
1248 test method starts running:
1249
1250 :cvar str config_file: This points to the main zuul config file
1251 within the fixtures directory. Subclasses may override this
1252 to obtain a different behavior.
1253
1254 :cvar str tenant_config_file: This is the tenant config file
1255 (which specifies from what git repos the configuration should
1256 be loaded). It defaults to the value specified in
1257 `config_file` but can be overidden by subclasses to obtain a
1258 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001259 configuration. See also the :py:func:`simple_layout`
1260 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001261
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001262 :cvar bool create_project_keys: Indicates whether Zuul should
1263 auto-generate keys for each project, or whether the test
1264 infrastructure should insert dummy keys to save time during
1265 startup. Defaults to False.
1266
James E. Blaire7b99a02016-08-05 14:27:34 -07001267 The following are instance variables that are useful within test
1268 methods:
1269
1270 :ivar FakeGerritConnection fake_<connection>:
1271 A :py:class:`~tests.base.FakeGerritConnection` will be
1272 instantiated for each connection present in the config file
1273 and stored here. For instance, `fake_gerrit` will hold the
1274 FakeGerritConnection object for a connection named `gerrit`.
1275
1276 :ivar FakeGearmanServer gearman_server: An instance of
1277 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1278 server that all of the Zuul components in this test use to
1279 communicate with each other.
1280
Paul Belanger174a8272017-03-14 13:20:10 -04001281 :ivar RecordingExecutorServer executor_server: An instance of
1282 :py:class:`~tests.base.RecordingExecutorServer` which is the
1283 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001284
1285 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1286 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001287 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001288 list upon completion.
1289
1290 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1291 objects representing completed builds. They are appended to
1292 the list in the order they complete.
1293
1294 """
1295
James E. Blair83005782015-12-11 14:46:03 -08001296 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001297 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001298 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001299
1300 def _startMerger(self):
1301 self.merge_server = zuul.merger.server.MergeServer(self.config,
1302 self.connections)
1303 self.merge_server.start()
1304
Maru Newby3fe5f852015-01-13 04:22:14 +00001305 def setUp(self):
1306 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001307
1308 self.setupZK()
1309
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001310 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001311 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001312 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1313 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001314 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001315 tmp_root = tempfile.mkdtemp(
1316 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001317 self.test_root = os.path.join(tmp_root, "zuul-test")
1318 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001319 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001320 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001321 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001322
1323 if os.path.exists(self.test_root):
1324 shutil.rmtree(self.test_root)
1325 os.makedirs(self.test_root)
1326 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001327 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001328
1329 # Make per test copy of Configuration.
1330 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001331 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001332 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001333 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001334 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001335 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001336 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001337
Clark Boylanb640e052014-04-03 16:41:46 -07001338 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001339 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1340 # see: https://github.com/jsocol/pystatsd/issues/61
1341 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001342 os.environ['STATSD_PORT'] = str(self.statsd.port)
1343 self.statsd.start()
1344 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001345 reload_module(statsd)
1346 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001347
1348 self.gearman_server = FakeGearmanServer()
1349
1350 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001351 self.log.info("Gearman server on port %s" %
1352 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001353
James E. Blaire511d2f2016-12-08 15:22:26 -08001354 gerritsource.GerritSource.replication_timeout = 1.5
1355 gerritsource.GerritSource.replication_retry_interval = 0.5
1356 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001357
Joshua Hesketh352264b2015-08-11 23:42:08 +10001358 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001359
Jan Hruban6b71aff2015-10-22 16:58:08 +02001360 self.event_queues = [
1361 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001362 self.sched.trigger_event_queue,
1363 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001364 ]
1365
James E. Blairfef78942016-03-11 16:28:56 -08001366 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001367 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001368
Clark Boylanb640e052014-04-03 16:41:46 -07001369 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001370 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001371 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001372 return FakeURLOpener(self.upstream_root, *args, **kw)
1373
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001374 old_urlopen = urllib.request.urlopen
1375 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001376
James E. Blair3f876d52016-07-22 13:07:14 -07001377 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001378
Paul Belanger174a8272017-03-14 13:20:10 -04001379 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001380 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001381 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001382 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001383 _test_root=self.test_root,
1384 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001385 self.executor_server.start()
1386 self.history = self.executor_server.build_history
1387 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001388
Paul Belanger174a8272017-03-14 13:20:10 -04001389 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001390 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001391 self.merge_client = zuul.merger.client.MergeClient(
1392 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001393 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001394 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001395 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001396
James E. Blair0d5a36e2017-02-21 10:53:44 -05001397 self.fake_nodepool = FakeNodepool(
1398 self.zk_chroot_fixture.zookeeper_host,
1399 self.zk_chroot_fixture.zookeeper_port,
1400 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001401
Paul Belanger174a8272017-03-14 13:20:10 -04001402 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001403 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001404 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001405 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001406
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001407 self.webapp = zuul.webapp.WebApp(
1408 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001409 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001410
1411 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001412 self.webapp.start()
1413 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001414 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001415 # Cleanups are run in reverse order
1416 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001417 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001418 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001419
James E. Blairb9c0d772017-03-03 14:34:49 -08001420 self.sched.reconfigure(self.config)
1421 self.sched.resume()
1422
James E. Blairfef78942016-03-11 16:28:56 -08001423 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001424 # Set up gerrit related fakes
1425 # Set a changes database so multiple FakeGerrit's can report back to
1426 # a virtual canonical database given by the configured hostname
1427 self.gerrit_changes_dbs = {}
1428
1429 def getGerritConnection(driver, name, config):
1430 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1431 con = FakeGerritConnection(driver, name, config,
1432 changes_db=db,
1433 upstream_root=self.upstream_root)
1434 self.event_queues.append(con.event_queue)
1435 setattr(self, 'fake_' + name, con)
1436 return con
1437
1438 self.useFixture(fixtures.MonkeyPatch(
1439 'zuul.driver.gerrit.GerritDriver.getConnection',
1440 getGerritConnection))
1441
1442 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001443 # TODO(jhesketh): This should come from lib.connections for better
1444 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001445 # Register connections from the config
1446 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001447
Joshua Hesketh352264b2015-08-11 23:42:08 +10001448 def FakeSMTPFactory(*args, **kw):
1449 args = [self.smtp_messages] + list(args)
1450 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001451
Joshua Hesketh352264b2015-08-11 23:42:08 +10001452 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001453
James E. Blaire511d2f2016-12-08 15:22:26 -08001454 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001455 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001456 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001457
James E. Blair83005782015-12-11 14:46:03 -08001458 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001459 # This creates the per-test configuration object. It can be
1460 # overriden by subclasses, but should not need to be since it
1461 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001462 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001463 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001464
1465 if not self.setupSimpleLayout():
1466 if hasattr(self, 'tenant_config_file'):
1467 self.config.set('zuul', 'tenant_config',
1468 self.tenant_config_file)
1469 git_path = os.path.join(
1470 os.path.dirname(
1471 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1472 'git')
1473 if os.path.exists(git_path):
1474 for reponame in os.listdir(git_path):
1475 project = reponame.replace('_', '/')
1476 self.copyDirToRepo(project,
1477 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001478 self.setupAllProjectKeys()
1479
James E. Blair06cc3922017-04-19 10:08:10 -07001480 def setupSimpleLayout(self):
1481 # If the test method has been decorated with a simple_layout,
1482 # use that instead of the class tenant_config_file. Set up a
1483 # single config-project with the specified layout, and
1484 # initialize repos for all of the 'project' entries which
1485 # appear in the layout.
1486 test_name = self.id().split('.')[-1]
1487 test = getattr(self, test_name)
1488 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001489 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001490 else:
1491 return False
1492
James E. Blairb70e55a2017-04-19 12:57:02 -07001493 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001494 path = os.path.join(FIXTURE_DIR, path)
1495 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001496 data = f.read()
1497 layout = yaml.safe_load(data)
1498 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001499 untrusted_projects = []
1500 for item in layout:
1501 if 'project' in item:
1502 name = item['project']['name']
1503 untrusted_projects.append(name)
1504 self.init_repo(name)
1505 self.addCommitToRepo(name, 'initial commit',
1506 files={'README': ''},
1507 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001508 if 'job' in item:
1509 jobname = item['job']['name']
1510 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001511
1512 root = os.path.join(self.test_root, "config")
1513 if not os.path.exists(root):
1514 os.makedirs(root)
1515 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1516 config = [{'tenant':
1517 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001518 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001519 {'config-projects': ['common-config'],
1520 'untrusted-projects': untrusted_projects}}}}]
1521 f.write(yaml.dump(config))
1522 f.close()
1523 self.config.set('zuul', 'tenant_config',
1524 os.path.join(FIXTURE_DIR, f.name))
1525
1526 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001527 self.addCommitToRepo('common-config', 'add content from fixture',
1528 files, branch='master', tag='init')
1529
1530 return True
1531
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001532 def setupAllProjectKeys(self):
1533 if self.create_project_keys:
1534 return
1535
1536 path = self.config.get('zuul', 'tenant_config')
1537 with open(os.path.join(FIXTURE_DIR, path)) as f:
1538 tenant_config = yaml.safe_load(f.read())
1539 for tenant in tenant_config:
1540 sources = tenant['tenant']['source']
1541 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001542 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001543 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001544 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001545 self.setupProjectKeys(source, project)
1546
1547 def setupProjectKeys(self, source, project):
1548 # Make sure we set up an RSA key for the project so that we
1549 # don't spend time generating one:
1550
1551 key_root = os.path.join(self.state_root, 'keys')
1552 if not os.path.isdir(key_root):
1553 os.mkdir(key_root, 0o700)
1554 private_key_file = os.path.join(key_root, source, project + '.pem')
1555 private_key_dir = os.path.dirname(private_key_file)
1556 self.log.debug("Installing test keys for project %s at %s" % (
1557 project, private_key_file))
1558 if not os.path.isdir(private_key_dir):
1559 os.makedirs(private_key_dir)
1560 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1561 with open(private_key_file, 'w') as o:
1562 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001563
James E. Blair498059b2016-12-20 13:50:13 -08001564 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001565 self.zk_chroot_fixture = self.useFixture(
1566 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001567 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001568 self.zk_chroot_fixture.zookeeper_host,
1569 self.zk_chroot_fixture.zookeeper_port,
1570 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001571
James E. Blair96c6bf82016-01-15 16:20:40 -08001572 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001573 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001574
1575 files = {}
1576 for (dirpath, dirnames, filenames) in os.walk(source_path):
1577 for filename in filenames:
1578 test_tree_filepath = os.path.join(dirpath, filename)
1579 common_path = os.path.commonprefix([test_tree_filepath,
1580 source_path])
1581 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1582 with open(test_tree_filepath, 'r') as f:
1583 content = f.read()
1584 files[relative_filepath] = content
1585 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001586 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001587
James E. Blaire18d4602017-01-05 11:17:28 -08001588 def assertNodepoolState(self):
1589 # Make sure that there are no pending requests
1590
1591 requests = self.fake_nodepool.getNodeRequests()
1592 self.assertEqual(len(requests), 0)
1593
1594 nodes = self.fake_nodepool.getNodes()
1595 for node in nodes:
1596 self.assertFalse(node['_lock'], "Node %s is locked" %
1597 (node['_oid'],))
1598
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001599 def assertNoGeneratedKeys(self):
1600 # Make sure that Zuul did not generate any project keys
1601 # (unless it was supposed to).
1602
1603 if self.create_project_keys:
1604 return
1605
1606 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1607 test_key = i.read()
1608
1609 key_root = os.path.join(self.state_root, 'keys')
1610 for root, dirname, files in os.walk(key_root):
1611 for fn in files:
1612 with open(os.path.join(root, fn)) as f:
1613 self.assertEqual(test_key, f.read())
1614
Clark Boylanb640e052014-04-03 16:41:46 -07001615 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001616 self.log.debug("Assert final state")
1617 # Make sure no jobs are running
1618 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001619 # Make sure that git.Repo objects have been garbage collected.
1620 repos = []
1621 gc.collect()
1622 for obj in gc.get_objects():
1623 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001624 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001625 repos.append(obj)
1626 self.assertEqual(len(repos), 0)
1627 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001628 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001629 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001630 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001631 for tenant in self.sched.abide.tenants.values():
1632 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001633 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001634 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001635
1636 def shutdown(self):
1637 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001638 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001639 self.merge_server.stop()
1640 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001641 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001642 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001643 self.sched.stop()
1644 self.sched.join()
1645 self.statsd.stop()
1646 self.statsd.join()
1647 self.webapp.stop()
1648 self.webapp.join()
1649 self.rpc.stop()
1650 self.rpc.join()
1651 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001652 self.fake_nodepool.stop()
1653 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001654 threads = threading.enumerate()
1655 if len(threads) > 1:
1656 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001657 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001658
James E. Blaira002b032017-04-18 10:35:48 -07001659 def assertCleanShutdown(self):
1660 pass
1661
Clark Boylanb640e052014-04-03 16:41:46 -07001662 def init_repo(self, project):
1663 parts = project.split('/')
1664 path = os.path.join(self.upstream_root, *parts[:-1])
1665 if not os.path.exists(path):
1666 os.makedirs(path)
1667 path = os.path.join(self.upstream_root, project)
1668 repo = git.Repo.init(path)
1669
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001670 with repo.config_writer() as config_writer:
1671 config_writer.set_value('user', 'email', 'user@example.com')
1672 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001673
Clark Boylanb640e052014-04-03 16:41:46 -07001674 repo.index.commit('initial commit')
1675 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001676
James E. Blair97d902e2014-08-21 13:25:56 -07001677 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001678 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001679 repo.git.clean('-x', '-f', '-d')
1680
James E. Blair97d902e2014-08-21 13:25:56 -07001681 def create_branch(self, project, branch):
1682 path = os.path.join(self.upstream_root, project)
1683 repo = git.Repo.init(path)
1684 fn = os.path.join(path, 'README')
1685
1686 branch_head = repo.create_head(branch)
1687 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001688 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001689 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001690 f.close()
1691 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001692 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001693
James E. Blair97d902e2014-08-21 13:25:56 -07001694 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001695 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001696 repo.git.clean('-x', '-f', '-d')
1697
Sachi King9f16d522016-03-16 12:20:45 +11001698 def create_commit(self, project):
1699 path = os.path.join(self.upstream_root, project)
1700 repo = git.Repo(path)
1701 repo.head.reference = repo.heads['master']
1702 file_name = os.path.join(path, 'README')
1703 with open(file_name, 'a') as f:
1704 f.write('creating fake commit\n')
1705 repo.index.add([file_name])
1706 commit = repo.index.commit('Creating a fake commit')
1707 return commit.hexsha
1708
James E. Blairf4a5f022017-04-18 14:01:10 -07001709 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001710 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001711 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001712 while len(self.builds):
1713 self.release(self.builds[0])
1714 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001715 i += 1
1716 if count is not None and i >= count:
1717 break
James E. Blairb8c16472015-05-05 14:55:26 -07001718
Clark Boylanb640e052014-04-03 16:41:46 -07001719 def release(self, job):
1720 if isinstance(job, FakeBuild):
1721 job.release()
1722 else:
1723 job.waiting = False
1724 self.log.debug("Queued job %s released" % job.unique)
1725 self.gearman_server.wakeConnections()
1726
1727 def getParameter(self, job, name):
1728 if isinstance(job, FakeBuild):
1729 return job.parameters[name]
1730 else:
1731 parameters = json.loads(job.arguments)
1732 return parameters[name]
1733
Clark Boylanb640e052014-04-03 16:41:46 -07001734 def haveAllBuildsReported(self):
1735 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001736 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001737 return False
1738 # Find out if every build that the worker has completed has been
1739 # reported back to Zuul. If it hasn't then that means a Gearman
1740 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001741 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001742 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001743 if not zbuild:
1744 # It has already been reported
1745 continue
1746 # It hasn't been reported yet.
1747 return False
1748 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001749 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001750 if connection.state == 'GRAB_WAIT':
1751 return False
1752 return True
1753
1754 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001755 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001756 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001757 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001758 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001759 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001760 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001761 for j in conn.related_jobs.values():
1762 if j.unique == build.uuid:
1763 client_job = j
1764 break
1765 if not client_job:
1766 self.log.debug("%s is not known to the gearman client" %
1767 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001768 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001769 if not client_job.handle:
1770 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001771 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001772 server_job = self.gearman_server.jobs.get(client_job.handle)
1773 if not server_job:
1774 self.log.debug("%s is not known to the gearman server" %
1775 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001776 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001777 if not hasattr(server_job, 'waiting'):
1778 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001779 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001780 if server_job.waiting:
1781 continue
James E. Blair17302972016-08-10 16:11:42 -07001782 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001783 self.log.debug("%s has not reported start" % build)
1784 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001785 worker_build = self.executor_server.job_builds.get(
1786 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001787 if worker_build:
1788 if worker_build.isWaiting():
1789 continue
1790 else:
1791 self.log.debug("%s is running" % worker_build)
1792 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001793 else:
James E. Blair962220f2016-08-03 11:22:38 -07001794 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001795 return False
James E. Blaira002b032017-04-18 10:35:48 -07001796 for (build_uuid, job_worker) in \
1797 self.executor_server.job_workers.items():
1798 if build_uuid not in seen_builds:
1799 self.log.debug("%s is not finalized" % build_uuid)
1800 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001801 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001802
James E. Blairdce6cea2016-12-20 16:45:32 -08001803 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001804 if self.fake_nodepool.paused:
1805 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001806 if self.sched.nodepool.requests:
1807 return False
1808 return True
1809
Jan Hruban6b71aff2015-10-22 16:58:08 +02001810 def eventQueuesEmpty(self):
1811 for queue in self.event_queues:
1812 yield queue.empty()
1813
1814 def eventQueuesJoin(self):
1815 for queue in self.event_queues:
1816 queue.join()
1817
Clark Boylanb640e052014-04-03 16:41:46 -07001818 def waitUntilSettled(self):
1819 self.log.debug("Waiting until settled...")
1820 start = time.time()
1821 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001822 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001823 self.log.error("Timeout waiting for Zuul to settle")
1824 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001825 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001826 self.log.error(" %s: %s" % (queue, queue.empty()))
1827 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001828 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001829 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001830 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001831 self.log.error("All requests completed: %s" %
1832 (self.areAllNodeRequestsComplete(),))
1833 self.log.error("Merge client jobs: %s" %
1834 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001835 raise Exception("Timeout waiting for Zuul to settle")
1836 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001837
Paul Belanger174a8272017-03-14 13:20:10 -04001838 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001839 # have all build states propogated to zuul?
1840 if self.haveAllBuildsReported():
1841 # Join ensures that the queue is empty _and_ events have been
1842 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001843 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001844 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001845 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001846 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001847 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001848 self.areAllNodeRequestsComplete() and
1849 all(self.eventQueuesEmpty())):
1850 # The queue empty check is placed at the end to
1851 # ensure that if a component adds an event between
1852 # when locked the run handler and checked that the
1853 # components were stable, we don't erroneously
1854 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001855 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001856 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001857 self.log.debug("...settled.")
1858 return
1859 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001860 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001861 self.sched.wake_event.wait(0.1)
1862
1863 def countJobResults(self, jobs, result):
1864 jobs = filter(lambda x: x.result == result, jobs)
1865 return len(jobs)
1866
James E. Blair96c6bf82016-01-15 16:20:40 -08001867 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001868 for job in self.history:
1869 if (job.name == name and
1870 (project is None or
1871 job.parameters['ZUUL_PROJECT'] == project)):
1872 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001873 raise Exception("Unable to find job %s in history" % name)
1874
1875 def assertEmptyQueues(self):
1876 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001877 for tenant in self.sched.abide.tenants.values():
1878 for pipeline in tenant.layout.pipelines.values():
1879 for queue in pipeline.queues:
1880 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001881 print('pipeline %s queue %s contents %s' % (
1882 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001883 self.assertEqual(len(queue.queue), 0,
1884 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001885
1886 def assertReportedStat(self, key, value=None, kind=None):
1887 start = time.time()
1888 while time.time() < (start + 5):
1889 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001890 k, v = stat.split(':')
1891 if key == k:
1892 if value is None and kind is None:
1893 return
1894 elif value:
1895 if value == v:
1896 return
1897 elif kind:
1898 if v.endswith('|' + kind):
1899 return
1900 time.sleep(0.1)
1901
Clark Boylanb640e052014-04-03 16:41:46 -07001902 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001903
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001904 def assertBuilds(self, builds):
1905 """Assert that the running builds are as described.
1906
1907 The list of running builds is examined and must match exactly
1908 the list of builds described by the input.
1909
1910 :arg list builds: A list of dictionaries. Each item in the
1911 list must match the corresponding build in the build
1912 history, and each element of the dictionary must match the
1913 corresponding attribute of the build.
1914
1915 """
James E. Blair3158e282016-08-19 09:34:11 -07001916 try:
1917 self.assertEqual(len(self.builds), len(builds))
1918 for i, d in enumerate(builds):
1919 for k, v in d.items():
1920 self.assertEqual(
1921 getattr(self.builds[i], k), v,
1922 "Element %i in builds does not match" % (i,))
1923 except Exception:
1924 for build in self.builds:
1925 self.log.error("Running build: %s" % build)
1926 else:
1927 self.log.error("No running builds")
1928 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001929
James E. Blairb536ecc2016-08-31 10:11:42 -07001930 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001931 """Assert that the completed builds are as described.
1932
1933 The list of completed builds is examined and must match
1934 exactly the list of builds described by the input.
1935
1936 :arg list history: A list of dictionaries. Each item in the
1937 list must match the corresponding build in the build
1938 history, and each element of the dictionary must match the
1939 corresponding attribute of the build.
1940
James E. Blairb536ecc2016-08-31 10:11:42 -07001941 :arg bool ordered: If true, the history must match the order
1942 supplied, if false, the builds are permitted to have
1943 arrived in any order.
1944
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001945 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001946 def matches(history_item, item):
1947 for k, v in item.items():
1948 if getattr(history_item, k) != v:
1949 return False
1950 return True
James E. Blair3158e282016-08-19 09:34:11 -07001951 try:
1952 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001953 if ordered:
1954 for i, d in enumerate(history):
1955 if not matches(self.history[i], d):
1956 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001957 "Element %i in history does not match %s" %
1958 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001959 else:
1960 unseen = self.history[:]
1961 for i, d in enumerate(history):
1962 found = False
1963 for unseen_item in unseen:
1964 if matches(unseen_item, d):
1965 found = True
1966 unseen.remove(unseen_item)
1967 break
1968 if not found:
1969 raise Exception("No match found for element %i "
1970 "in history" % (i,))
1971 if unseen:
1972 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001973 except Exception:
1974 for build in self.history:
1975 self.log.error("Completed build: %s" % build)
1976 else:
1977 self.log.error("No completed builds")
1978 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001979
James E. Blair6ac368c2016-12-22 18:07:20 -08001980 def printHistory(self):
1981 """Log the build history.
1982
1983 This can be useful during tests to summarize what jobs have
1984 completed.
1985
1986 """
1987 self.log.debug("Build history:")
1988 for build in self.history:
1989 self.log.debug(build)
1990
James E. Blair59fdbac2015-12-07 17:08:06 -08001991 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001992 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1993
James E. Blair109da3f2017-04-04 14:39:43 -07001994 def updateConfigLayout(self, path, untrusted_projects=None):
1995 if untrusted_projects is None:
1996 untrusted_projects = []
James E. Blairf84026c2015-12-08 16:11:46 -08001997 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001998 if not os.path.exists(root):
1999 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002000 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2001 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002002- tenant:
2003 name: openstack
2004 source:
2005 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002006 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002007 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002008 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002009 - org/project
2010 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002011 - org/project2\n""" % path)
James E. Blair0ffa0102017-03-30 13:11:33 -07002012
James E. Blair109da3f2017-04-04 14:39:43 -07002013 for repo in untrusted_projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002014 f.write(" - %s\n" % repo)
James E. Blairf84026c2015-12-08 16:11:46 -08002015 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002016 self.config.set('zuul', 'tenant_config',
2017 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002018 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002019
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002020 def addCommitToRepo(self, project, message, files,
2021 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002022 path = os.path.join(self.upstream_root, project)
2023 repo = git.Repo(path)
2024 repo.head.reference = branch
2025 zuul.merger.merger.reset_repo_to_head(repo)
2026 for fn, content in files.items():
2027 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002028 try:
2029 os.makedirs(os.path.dirname(fn))
2030 except OSError:
2031 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002032 with open(fn, 'w') as f:
2033 f.write(content)
2034 repo.index.add([fn])
2035 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002036 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002037 repo.heads[branch].commit = commit
2038 repo.head.reference = branch
2039 repo.git.clean('-x', '-f', '-d')
2040 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002041 if tag:
2042 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002043 return before
2044
2045 def commitLayoutUpdate(self, orig_name, source_name):
2046 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002047 source_name)
2048 to_copy = ['zuul.yaml']
2049 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2050 to_copy.append('playbooks/{}'.format(playbook))
2051 commit_data = {}
2052 for source_file in to_copy:
2053 source_file_path = os.path.join(source_path, source_file)
2054 with open(source_file_path, 'r') as nt:
2055 commit_data[source_file] = nt.read()
2056 before = self.addCommitToRepo(
2057 orig_name, 'Pulling content from %s' % source_name,
2058 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002059 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002060
James E. Blair7fc8daa2016-08-08 15:37:15 -07002061 def addEvent(self, connection, event):
2062 """Inject a Fake (Gerrit) event.
2063
2064 This method accepts a JSON-encoded event and simulates Zuul
2065 having received it from Gerrit. It could (and should)
2066 eventually apply to any connection type, but is currently only
2067 used with Gerrit connections. The name of the connection is
2068 used to look up the corresponding server, and the event is
2069 simulated as having been received by all Zuul connections
2070 attached to that server. So if two Gerrit connections in Zuul
2071 are connected to the same Gerrit server, and you invoke this
2072 method specifying the name of one of them, the event will be
2073 received by both.
2074
2075 .. note::
2076
2077 "self.fake_gerrit.addEvent" calls should be migrated to
2078 this method.
2079
2080 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002081 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002082 :arg str event: The JSON-encoded event.
2083
2084 """
2085 specified_conn = self.connections.connections[connection]
2086 for conn in self.connections.connections.values():
2087 if (isinstance(conn, specified_conn.__class__) and
2088 specified_conn.server == conn.server):
2089 conn.addEvent(event)
2090
James E. Blair3f876d52016-07-22 13:07:14 -07002091
2092class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002093 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002094 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002095
Joshua Heskethd78b4482015-09-14 16:56:34 -06002096
2097class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002098 def setup_config(self):
2099 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002100 for section_name in self.config.sections():
2101 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2102 section_name, re.I)
2103 if not con_match:
2104 continue
2105
2106 if self.config.get(section_name, 'driver') == 'sql':
2107 f = MySQLSchemaFixture()
2108 self.useFixture(f)
2109 if (self.config.get(section_name, 'dburi') ==
2110 '$MYSQL_FIXTURE_DBURI$'):
2111 self.config.set(section_name, 'dburi', f.dburi)