blob: 6c5ab2c56f1b1344207efdaaba5838d31deecd34 [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
James E. Blairc4ba97a2017-04-19 16:26:24 -07001662 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001663 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')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001676 if tag:
1677 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001678
James E. Blair97d902e2014-08-21 13:25:56 -07001679 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001680 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001681 repo.git.clean('-x', '-f', '-d')
1682
James E. Blair97d902e2014-08-21 13:25:56 -07001683 def create_branch(self, project, branch):
1684 path = os.path.join(self.upstream_root, project)
1685 repo = git.Repo.init(path)
1686 fn = os.path.join(path, 'README')
1687
1688 branch_head = repo.create_head(branch)
1689 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001690 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001691 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001692 f.close()
1693 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001694 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001695
James E. Blair97d902e2014-08-21 13:25:56 -07001696 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001697 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001698 repo.git.clean('-x', '-f', '-d')
1699
Sachi King9f16d522016-03-16 12:20:45 +11001700 def create_commit(self, project):
1701 path = os.path.join(self.upstream_root, project)
1702 repo = git.Repo(path)
1703 repo.head.reference = repo.heads['master']
1704 file_name = os.path.join(path, 'README')
1705 with open(file_name, 'a') as f:
1706 f.write('creating fake commit\n')
1707 repo.index.add([file_name])
1708 commit = repo.index.commit('Creating a fake commit')
1709 return commit.hexsha
1710
James E. Blairf4a5f022017-04-18 14:01:10 -07001711 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001712 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001713 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001714 while len(self.builds):
1715 self.release(self.builds[0])
1716 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001717 i += 1
1718 if count is not None and i >= count:
1719 break
James E. Blairb8c16472015-05-05 14:55:26 -07001720
Clark Boylanb640e052014-04-03 16:41:46 -07001721 def release(self, job):
1722 if isinstance(job, FakeBuild):
1723 job.release()
1724 else:
1725 job.waiting = False
1726 self.log.debug("Queued job %s released" % job.unique)
1727 self.gearman_server.wakeConnections()
1728
1729 def getParameter(self, job, name):
1730 if isinstance(job, FakeBuild):
1731 return job.parameters[name]
1732 else:
1733 parameters = json.loads(job.arguments)
1734 return parameters[name]
1735
Clark Boylanb640e052014-04-03 16:41:46 -07001736 def haveAllBuildsReported(self):
1737 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001738 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001739 return False
1740 # Find out if every build that the worker has completed has been
1741 # reported back to Zuul. If it hasn't then that means a Gearman
1742 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001743 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001744 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001745 if not zbuild:
1746 # It has already been reported
1747 continue
1748 # It hasn't been reported yet.
1749 return False
1750 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001751 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001752 if connection.state == 'GRAB_WAIT':
1753 return False
1754 return True
1755
1756 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001757 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001758 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001759 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001760 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001761 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001762 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001763 for j in conn.related_jobs.values():
1764 if j.unique == build.uuid:
1765 client_job = j
1766 break
1767 if not client_job:
1768 self.log.debug("%s is not known to the gearman client" %
1769 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001770 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001771 if not client_job.handle:
1772 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001773 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001774 server_job = self.gearman_server.jobs.get(client_job.handle)
1775 if not server_job:
1776 self.log.debug("%s is not known to the gearman server" %
1777 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001778 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001779 if not hasattr(server_job, 'waiting'):
1780 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001781 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001782 if server_job.waiting:
1783 continue
James E. Blair17302972016-08-10 16:11:42 -07001784 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001785 self.log.debug("%s has not reported start" % build)
1786 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001787 worker_build = self.executor_server.job_builds.get(
1788 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001789 if worker_build:
1790 if worker_build.isWaiting():
1791 continue
1792 else:
1793 self.log.debug("%s is running" % worker_build)
1794 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001795 else:
James E. Blair962220f2016-08-03 11:22:38 -07001796 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001797 return False
James E. Blaira002b032017-04-18 10:35:48 -07001798 for (build_uuid, job_worker) in \
1799 self.executor_server.job_workers.items():
1800 if build_uuid not in seen_builds:
1801 self.log.debug("%s is not finalized" % build_uuid)
1802 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001803 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001804
James E. Blairdce6cea2016-12-20 16:45:32 -08001805 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001806 if self.fake_nodepool.paused:
1807 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001808 if self.sched.nodepool.requests:
1809 return False
1810 return True
1811
Jan Hruban6b71aff2015-10-22 16:58:08 +02001812 def eventQueuesEmpty(self):
1813 for queue in self.event_queues:
1814 yield queue.empty()
1815
1816 def eventQueuesJoin(self):
1817 for queue in self.event_queues:
1818 queue.join()
1819
Clark Boylanb640e052014-04-03 16:41:46 -07001820 def waitUntilSettled(self):
1821 self.log.debug("Waiting until settled...")
1822 start = time.time()
1823 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001824 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001825 self.log.error("Timeout waiting for Zuul to settle")
1826 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001827 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001828 self.log.error(" %s: %s" % (queue, queue.empty()))
1829 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001830 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001831 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001832 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001833 self.log.error("All requests completed: %s" %
1834 (self.areAllNodeRequestsComplete(),))
1835 self.log.error("Merge client jobs: %s" %
1836 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001837 raise Exception("Timeout waiting for Zuul to settle")
1838 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001839
Paul Belanger174a8272017-03-14 13:20:10 -04001840 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001841 # have all build states propogated to zuul?
1842 if self.haveAllBuildsReported():
1843 # Join ensures that the queue is empty _and_ events have been
1844 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001845 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001846 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001847 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001848 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001849 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001850 self.areAllNodeRequestsComplete() and
1851 all(self.eventQueuesEmpty())):
1852 # The queue empty check is placed at the end to
1853 # ensure that if a component adds an event between
1854 # when locked the run handler and checked that the
1855 # components were stable, we don't erroneously
1856 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001857 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001858 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001859 self.log.debug("...settled.")
1860 return
1861 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001862 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001863 self.sched.wake_event.wait(0.1)
1864
1865 def countJobResults(self, jobs, result):
1866 jobs = filter(lambda x: x.result == result, jobs)
1867 return len(jobs)
1868
James E. Blair96c6bf82016-01-15 16:20:40 -08001869 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001870 for job in self.history:
1871 if (job.name == name and
1872 (project is None or
1873 job.parameters['ZUUL_PROJECT'] == project)):
1874 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001875 raise Exception("Unable to find job %s in history" % name)
1876
1877 def assertEmptyQueues(self):
1878 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001879 for tenant in self.sched.abide.tenants.values():
1880 for pipeline in tenant.layout.pipelines.values():
1881 for queue in pipeline.queues:
1882 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001883 print('pipeline %s queue %s contents %s' % (
1884 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001885 self.assertEqual(len(queue.queue), 0,
1886 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001887
1888 def assertReportedStat(self, key, value=None, kind=None):
1889 start = time.time()
1890 while time.time() < (start + 5):
1891 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001892 k, v = stat.split(':')
1893 if key == k:
1894 if value is None and kind is None:
1895 return
1896 elif value:
1897 if value == v:
1898 return
1899 elif kind:
1900 if v.endswith('|' + kind):
1901 return
1902 time.sleep(0.1)
1903
Clark Boylanb640e052014-04-03 16:41:46 -07001904 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001905
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001906 def assertBuilds(self, builds):
1907 """Assert that the running builds are as described.
1908
1909 The list of running builds is examined and must match exactly
1910 the list of builds described by the input.
1911
1912 :arg list builds: A list of dictionaries. Each item in the
1913 list must match the corresponding build in the build
1914 history, and each element of the dictionary must match the
1915 corresponding attribute of the build.
1916
1917 """
James E. Blair3158e282016-08-19 09:34:11 -07001918 try:
1919 self.assertEqual(len(self.builds), len(builds))
1920 for i, d in enumerate(builds):
1921 for k, v in d.items():
1922 self.assertEqual(
1923 getattr(self.builds[i], k), v,
1924 "Element %i in builds does not match" % (i,))
1925 except Exception:
1926 for build in self.builds:
1927 self.log.error("Running build: %s" % build)
1928 else:
1929 self.log.error("No running builds")
1930 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001931
James E. Blairb536ecc2016-08-31 10:11:42 -07001932 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001933 """Assert that the completed builds are as described.
1934
1935 The list of completed builds is examined and must match
1936 exactly the list of builds described by the input.
1937
1938 :arg list history: A list of dictionaries. Each item in the
1939 list must match the corresponding build in the build
1940 history, and each element of the dictionary must match the
1941 corresponding attribute of the build.
1942
James E. Blairb536ecc2016-08-31 10:11:42 -07001943 :arg bool ordered: If true, the history must match the order
1944 supplied, if false, the builds are permitted to have
1945 arrived in any order.
1946
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001947 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001948 def matches(history_item, item):
1949 for k, v in item.items():
1950 if getattr(history_item, k) != v:
1951 return False
1952 return True
James E. Blair3158e282016-08-19 09:34:11 -07001953 try:
1954 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001955 if ordered:
1956 for i, d in enumerate(history):
1957 if not matches(self.history[i], d):
1958 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001959 "Element %i in history does not match %s" %
1960 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001961 else:
1962 unseen = self.history[:]
1963 for i, d in enumerate(history):
1964 found = False
1965 for unseen_item in unseen:
1966 if matches(unseen_item, d):
1967 found = True
1968 unseen.remove(unseen_item)
1969 break
1970 if not found:
1971 raise Exception("No match found for element %i "
1972 "in history" % (i,))
1973 if unseen:
1974 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001975 except Exception:
1976 for build in self.history:
1977 self.log.error("Completed build: %s" % build)
1978 else:
1979 self.log.error("No completed builds")
1980 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001981
James E. Blair6ac368c2016-12-22 18:07:20 -08001982 def printHistory(self):
1983 """Log the build history.
1984
1985 This can be useful during tests to summarize what jobs have
1986 completed.
1987
1988 """
1989 self.log.debug("Build history:")
1990 for build in self.history:
1991 self.log.debug(build)
1992
James E. Blair59fdbac2015-12-07 17:08:06 -08001993 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001994 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1995
James E. Blair9ea70072017-04-19 16:05:30 -07001996 def updateConfigLayout(self, path):
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. Blairf84026c2015-12-08 16:11:46 -08002012 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002013 self.config.set('zuul', 'tenant_config',
2014 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002015 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002016
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002017 def addCommitToRepo(self, project, message, files,
2018 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002019 path = os.path.join(self.upstream_root, project)
2020 repo = git.Repo(path)
2021 repo.head.reference = branch
2022 zuul.merger.merger.reset_repo_to_head(repo)
2023 for fn, content in files.items():
2024 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002025 try:
2026 os.makedirs(os.path.dirname(fn))
2027 except OSError:
2028 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002029 with open(fn, 'w') as f:
2030 f.write(content)
2031 repo.index.add([fn])
2032 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002033 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002034 repo.heads[branch].commit = commit
2035 repo.head.reference = branch
2036 repo.git.clean('-x', '-f', '-d')
2037 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002038 if tag:
2039 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002040 return before
2041
2042 def commitLayoutUpdate(self, orig_name, source_name):
2043 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002044 source_name)
2045 to_copy = ['zuul.yaml']
2046 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2047 to_copy.append('playbooks/{}'.format(playbook))
2048 commit_data = {}
2049 for source_file in to_copy:
2050 source_file_path = os.path.join(source_path, source_file)
2051 with open(source_file_path, 'r') as nt:
2052 commit_data[source_file] = nt.read()
2053 before = self.addCommitToRepo(
2054 orig_name, 'Pulling content from %s' % source_name,
2055 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002056 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002057
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002058 def commitConfigUpdate(self, project_name, source_name):
2059 """Commit an update to zuul.yaml
2060
2061 This overwrites the zuul.yaml in the specificed project with
2062 the contents specified.
2063
2064 :arg str project_name: The name of the project containing
2065 zuul.yaml (e.g., common-config)
2066
2067 :arg str source_name: The path to the file (underneath the
2068 test fixture directory) whose contents should be used to
2069 replace zuul.yaml.
2070 """
2071
2072 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002073 files = {}
2074 with open(source_path, 'r') as f:
2075 data = f.read()
2076 layout = yaml.safe_load(data)
2077 files['zuul.yaml'] = data
2078 for item in layout:
2079 if 'job' in item:
2080 jobname = item['job']['name']
2081 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002082 before = self.addCommitToRepo(
2083 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002084 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002085 return before
2086
James E. Blair7fc8daa2016-08-08 15:37:15 -07002087 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002088
James E. Blair7fc8daa2016-08-08 15:37:15 -07002089 """Inject a Fake (Gerrit) event.
2090
2091 This method accepts a JSON-encoded event and simulates Zuul
2092 having received it from Gerrit. It could (and should)
2093 eventually apply to any connection type, but is currently only
2094 used with Gerrit connections. The name of the connection is
2095 used to look up the corresponding server, and the event is
2096 simulated as having been received by all Zuul connections
2097 attached to that server. So if two Gerrit connections in Zuul
2098 are connected to the same Gerrit server, and you invoke this
2099 method specifying the name of one of them, the event will be
2100 received by both.
2101
2102 .. note::
2103
2104 "self.fake_gerrit.addEvent" calls should be migrated to
2105 this method.
2106
2107 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002108 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002109 :arg str event: The JSON-encoded event.
2110
2111 """
2112 specified_conn = self.connections.connections[connection]
2113 for conn in self.connections.connections.values():
2114 if (isinstance(conn, specified_conn.__class__) and
2115 specified_conn.server == conn.server):
2116 conn.addEvent(event)
2117
James E. Blair3f876d52016-07-22 13:07:14 -07002118
2119class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002120 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002121 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002122
Joshua Heskethd78b4482015-09-14 16:56:34 -06002123
2124class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002125 def setup_config(self):
2126 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002127 for section_name in self.config.sections():
2128 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2129 section_name, re.I)
2130 if not con_match:
2131 continue
2132
2133 if self.config.get(section_name, 'driver') == 'sql':
2134 f = MySQLSchemaFixture()
2135 self.useFixture(f)
2136 if (self.config.get(section_name, 'dburi') ==
2137 '$MYSQL_FIXTURE_DBURI$'):
2138 self.config.set(section_name, 'dburi', f.dburi)