blob: c1e7349e11f1c5fe0f08e2507a030fa6e584eec5 [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
James E. Blair06cc3922017-04-19 10:08:10 -0700100def simple_layout(path):
101 """Specify a layout file for use by a test method.
102
103 :arg str path: The path to the layout file.
104
105 Some tests require only a very simple configuration. For those,
106 establishing a complete config directory hierachy is too much
107 work. In those cases, you can add a simple zuul.yaml file to the
108 test fixtures directory (in fixtures/layouts/foo.yaml) and use
109 this decorator to indicate the test method should use that rather
110 than the tenant config file specified by the test class.
111
112 The decorator will cause that layout file to be added to a
113 config-project called "common-config" and each "project" instance
114 referenced in the layout file will have a git repo automatically
115 initialized.
116 """
117
118 def decorator(test):
119 test.__simple_layout__ = path
120 return test
121 return decorator
122
123
Clark Boylanb640e052014-04-03 16:41:46 -0700124class ChangeReference(git.Reference):
125 _common_path_default = "refs/changes"
126 _points_to_commits_only = True
127
128
129class FakeChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700130 categories = {'approved': ('Approved', -1, 1),
131 'code-review': ('Code-Review', -2, 2),
132 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700133
134 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700135 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700136 self.gerrit = gerrit
137 self.reported = 0
138 self.queried = 0
139 self.patchsets = []
140 self.number = number
141 self.project = project
142 self.branch = branch
143 self.subject = subject
144 self.latest_patchset = 0
145 self.depends_on_change = None
146 self.needed_by_changes = []
147 self.fail_merge = False
148 self.messages = []
149 self.data = {
150 'branch': branch,
151 'comments': [],
152 'commitMessage': subject,
153 'createdOn': time.time(),
154 'id': 'I' + random_sha1(),
155 'lastUpdated': time.time(),
156 'number': str(number),
157 'open': status == 'NEW',
158 'owner': {'email': 'user@example.com',
159 'name': 'User Name',
160 'username': 'username'},
161 'patchSets': self.patchsets,
162 'project': project,
163 'status': status,
164 'subject': subject,
165 'submitRecords': [],
166 'url': 'https://hostname/%s' % number}
167
168 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700169 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700170 self.data['submitRecords'] = self.getSubmitRecords()
171 self.open = status == 'NEW'
172
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700173 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700174 path = os.path.join(self.upstream_root, self.project)
175 repo = git.Repo(path)
176 ref = ChangeReference.create(repo, '1/%s/%s' % (self.number,
177 self.latest_patchset),
178 'refs/tags/init')
179 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700180 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700181 repo.git.clean('-x', '-f', '-d')
182
183 path = os.path.join(self.upstream_root, self.project)
184 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700185 for fn, content in files.items():
186 fn = os.path.join(path, fn)
187 with open(fn, 'w') as f:
188 f.write(content)
189 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700190 else:
191 for fni in range(100):
192 fn = os.path.join(path, str(fni))
193 f = open(fn, 'w')
194 for ci in range(4096):
195 f.write(random.choice(string.printable))
196 f.close()
197 repo.index.add([fn])
198
199 r = repo.index.commit(msg)
200 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700201 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700202 repo.git.clean('-x', '-f', '-d')
203 repo.heads['master'].checkout()
204 return r
205
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700206 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700207 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700208 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700209 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700210 data = ("test %s %s %s\n" %
211 (self.branch, self.number, self.latest_patchset))
212 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700213 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700215 ps_files = [{'file': '/COMMIT_MSG',
216 'type': 'ADDED'},
217 {'file': 'README',
218 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700219 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700220 ps_files.append({'file': f, 'type': 'ADDED'})
221 d = {'approvals': [],
222 'createdOn': time.time(),
223 'files': ps_files,
224 'number': str(self.latest_patchset),
225 'ref': 'refs/changes/1/%s/%s' % (self.number,
226 self.latest_patchset),
227 'revision': c.hexsha,
228 'uploader': {'email': 'user@example.com',
229 'name': 'User name',
230 'username': 'user'}}
231 self.data['currentPatchSet'] = d
232 self.patchsets.append(d)
233 self.data['submitRecords'] = self.getSubmitRecords()
234
235 def getPatchsetCreatedEvent(self, patchset):
236 event = {"type": "patchset-created",
237 "change": {"project": self.project,
238 "branch": self.branch,
239 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
240 "number": str(self.number),
241 "subject": self.subject,
242 "owner": {"name": "User Name"},
243 "url": "https://hostname/3"},
244 "patchSet": self.patchsets[patchset - 1],
245 "uploader": {"name": "User Name"}}
246 return event
247
248 def getChangeRestoredEvent(self):
249 event = {"type": "change-restored",
250 "change": {"project": self.project,
251 "branch": self.branch,
252 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
253 "number": str(self.number),
254 "subject": self.subject,
255 "owner": {"name": "User Name"},
256 "url": "https://hostname/3"},
257 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100258 "patchSet": self.patchsets[-1],
259 "reason": ""}
260 return event
261
262 def getChangeAbandonedEvent(self):
263 event = {"type": "change-abandoned",
264 "change": {"project": self.project,
265 "branch": self.branch,
266 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
267 "number": str(self.number),
268 "subject": self.subject,
269 "owner": {"name": "User Name"},
270 "url": "https://hostname/3"},
271 "abandoner": {"name": "User Name"},
272 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700273 "reason": ""}
274 return event
275
276 def getChangeCommentEvent(self, patchset):
277 event = {"type": "comment-added",
278 "change": {"project": self.project,
279 "branch": self.branch,
280 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
281 "number": str(self.number),
282 "subject": self.subject,
283 "owner": {"name": "User Name"},
284 "url": "https://hostname/3"},
285 "patchSet": self.patchsets[patchset - 1],
286 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700287 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700288 "description": "Code-Review",
289 "value": "0"}],
290 "comment": "This is a comment"}
291 return event
292
James E. Blairc2a5ed72017-02-20 14:12:01 -0500293 def getChangeMergedEvent(self):
294 event = {"submitter": {"name": "Jenkins",
295 "username": "jenkins"},
296 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
297 "patchSet": self.patchsets[-1],
298 "change": self.data,
299 "type": "change-merged",
300 "eventCreatedOn": 1487613810}
301 return event
302
James E. Blair8cce42e2016-10-18 08:18:36 -0700303 def getRefUpdatedEvent(self):
304 path = os.path.join(self.upstream_root, self.project)
305 repo = git.Repo(path)
306 oldrev = repo.heads[self.branch].commit.hexsha
307
308 event = {
309 "type": "ref-updated",
310 "submitter": {
311 "name": "User Name",
312 },
313 "refUpdate": {
314 "oldRev": oldrev,
315 "newRev": self.patchsets[-1]['revision'],
316 "refName": self.branch,
317 "project": self.project,
318 }
319 }
320 return event
321
Joshua Hesketh642824b2014-07-01 17:54:59 +1000322 def addApproval(self, category, value, username='reviewer_john',
323 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700324 if not granted_on:
325 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000326 approval = {
327 'description': self.categories[category][0],
328 'type': category,
329 'value': str(value),
330 'by': {
331 'username': username,
332 'email': username + '@example.com',
333 },
334 'grantedOn': int(granted_on)
335 }
Clark Boylanb640e052014-04-03 16:41:46 -0700336 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
337 if x['by']['username'] == username and x['type'] == category:
338 del self.patchsets[-1]['approvals'][i]
339 self.patchsets[-1]['approvals'].append(approval)
340 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000341 'author': {'email': 'author@example.com',
342 'name': 'Patchset Author',
343 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700344 'change': {'branch': self.branch,
345 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
346 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000347 'owner': {'email': 'owner@example.com',
348 'name': 'Change Owner',
349 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700350 'project': self.project,
351 'subject': self.subject,
352 'topic': 'master',
353 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700355 'patchSet': self.patchsets[-1],
356 'type': 'comment-added'}
357 self.data['submitRecords'] = self.getSubmitRecords()
358 return json.loads(json.dumps(event))
359
360 def getSubmitRecords(self):
361 status = {}
362 for cat in self.categories.keys():
363 status[cat] = 0
364
365 for a in self.patchsets[-1]['approvals']:
366 cur = status[a['type']]
367 cat_min, cat_max = self.categories[a['type']][1:]
368 new = int(a['value'])
369 if new == cat_min:
370 cur = new
371 elif abs(new) > abs(cur):
372 cur = new
373 status[a['type']] = cur
374
375 labels = []
376 ok = True
377 for typ, cat in self.categories.items():
378 cur = status[typ]
379 cat_min, cat_max = cat[1:]
380 if cur == cat_min:
381 value = 'REJECT'
382 ok = False
383 elif cur == cat_max:
384 value = 'OK'
385 else:
386 value = 'NEED'
387 ok = False
388 labels.append({'label': cat[0], 'status': value})
389 if ok:
390 return [{'status': 'OK'}]
391 return [{'status': 'NOT_READY',
392 'labels': labels}]
393
394 def setDependsOn(self, other, patchset):
395 self.depends_on_change = other
396 d = {'id': other.data['id'],
397 'number': other.data['number'],
398 'ref': other.patchsets[patchset - 1]['ref']
399 }
400 self.data['dependsOn'] = [d]
401
402 other.needed_by_changes.append(self)
403 needed = other.data.get('neededBy', [])
404 d = {'id': self.data['id'],
405 'number': self.data['number'],
406 'ref': self.patchsets[patchset - 1]['ref'],
407 'revision': self.patchsets[patchset - 1]['revision']
408 }
409 needed.append(d)
410 other.data['neededBy'] = needed
411
412 def query(self):
413 self.queried += 1
414 d = self.data.get('dependsOn')
415 if d:
416 d = d[0]
417 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
418 d['isCurrentPatchSet'] = True
419 else:
420 d['isCurrentPatchSet'] = False
421 return json.loads(json.dumps(self.data))
422
423 def setMerged(self):
424 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000425 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700426 return
427 if self.fail_merge:
428 return
429 self.data['status'] = 'MERGED'
430 self.open = False
431
432 path = os.path.join(self.upstream_root, self.project)
433 repo = git.Repo(path)
434 repo.heads[self.branch].commit = \
435 repo.commit(self.patchsets[-1]['revision'])
436
437 def setReported(self):
438 self.reported += 1
439
440
James E. Blaire511d2f2016-12-08 15:22:26 -0800441class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700442 """A Fake Gerrit connection for use in tests.
443
444 This subclasses
445 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
446 ability for tests to add changes to the fake Gerrit it represents.
447 """
448
Joshua Hesketh352264b2015-08-11 23:42:08 +1000449 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700450
James E. Blaire511d2f2016-12-08 15:22:26 -0800451 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700452 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800453 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000454 connection_config)
455
James E. Blair7fc8daa2016-08-08 15:37:15 -0700456 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700457 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
458 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000459 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700460 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200461 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700462
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700463 def addFakeChange(self, project, branch, subject, status='NEW',
464 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700465 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700466 self.change_number += 1
467 c = FakeChange(self, self.change_number, project, branch, subject,
468 upstream_root=self.upstream_root,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700469 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700470 self.changes[self.change_number] = c
471 return c
472
Clark Boylanb640e052014-04-03 16:41:46 -0700473 def review(self, project, changeid, message, action):
474 number, ps = changeid.split(',')
475 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000476
477 # Add the approval back onto the change (ie simulate what gerrit would
478 # do).
479 # Usually when zuul leaves a review it'll create a feedback loop where
480 # zuul's review enters another gerrit event (which is then picked up by
481 # zuul). However, we can't mimic this behaviour (by adding this
482 # approval event into the queue) as it stops jobs from checking what
483 # happens before this event is triggered. If a job needs to see what
484 # happens they can add their own verified event into the queue.
485 # Nevertheless, we can update change with the new review in gerrit.
486
James E. Blair8b5408c2016-08-08 15:37:46 -0700487 for cat in action.keys():
488 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000489 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000490
James E. Blair8b5408c2016-08-08 15:37:46 -0700491 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000492 if 'label' in action:
493 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000494 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000495
Clark Boylanb640e052014-04-03 16:41:46 -0700496 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 if 'submit' in action:
499 change.setMerged()
500 if message:
501 change.setReported()
502
503 def query(self, number):
504 change = self.changes.get(int(number))
505 if change:
506 return change.query()
507 return {}
508
James E. Blairc494d542014-08-06 09:23:52 -0700509 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700510 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700511 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800512 if query.startswith('change:'):
513 # Query a specific changeid
514 changeid = query[len('change:'):]
515 l = [change.query() for change in self.changes.values()
516 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700517 elif query.startswith('message:'):
518 # Query the content of a commit message
519 msg = query[len('message:'):].strip()
520 l = [change.query() for change in self.changes.values()
521 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800522 else:
523 # Query all open changes
524 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700525 return l
James E. Blairc494d542014-08-06 09:23:52 -0700526
Joshua Hesketh352264b2015-08-11 23:42:08 +1000527 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700528 pass
529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def getGitUrl(self, project):
531 return os.path.join(self.upstream_root, project.name)
532
Clark Boylanb640e052014-04-03 16:41:46 -0700533
534class BuildHistory(object):
535 def __init__(self, **kw):
536 self.__dict__.update(kw)
537
538 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700539 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
540 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700541
542
543class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200544 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700545 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700546 self.url = url
547
548 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700549 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700550 path = res.path
551 project = '/'.join(path.split('/')[2:-2])
552 ret = '001e# service=git-upload-pack\n'
553 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
554 'multi_ack thin-pack side-band side-band-64k ofs-delta '
555 'shallow no-progress include-tag multi_ack_detailed no-done\n')
556 path = os.path.join(self.upstream_root, project)
557 repo = git.Repo(path)
558 for ref in repo.refs:
559 r = ref.object.hexsha + ' ' + ref.path + '\n'
560 ret += '%04x%s' % (len(r) + 4, r)
561 ret += '0000'
562 return ret
563
564
Clark Boylanb640e052014-04-03 16:41:46 -0700565class FakeStatsd(threading.Thread):
566 def __init__(self):
567 threading.Thread.__init__(self)
568 self.daemon = True
569 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
570 self.sock.bind(('', 0))
571 self.port = self.sock.getsockname()[1]
572 self.wake_read, self.wake_write = os.pipe()
573 self.stats = []
574
575 def run(self):
576 while True:
577 poll = select.poll()
578 poll.register(self.sock, select.POLLIN)
579 poll.register(self.wake_read, select.POLLIN)
580 ret = poll.poll()
581 for (fd, event) in ret:
582 if fd == self.sock.fileno():
583 data = self.sock.recvfrom(1024)
584 if not data:
585 return
586 self.stats.append(data[0])
587 if fd == self.wake_read:
588 return
589
590 def stop(self):
591 os.write(self.wake_write, '1\n')
592
593
James E. Blaire1767bc2016-08-02 10:00:27 -0700594class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700595 log = logging.getLogger("zuul.test")
596
Paul Belanger174a8272017-03-14 13:20:10 -0400597 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700598 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400599 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700600 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700601 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700602 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700603 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700604 # TODOv3(jeblair): self.node is really "the image of the node
605 # assigned". We should rename it (self.node_image?) if we
606 # keep using it like this, or we may end up exposing more of
607 # the complexity around multi-node jobs here
608 # (self.nodes[0].image?)
609 self.node = None
610 if len(self.parameters.get('nodes')) == 1:
611 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700612 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100613 self.pipeline = self.parameters['ZUUL_PIPELINE']
614 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700615 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700616 self.wait_condition = threading.Condition()
617 self.waiting = False
618 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500619 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700620 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700621 self.changes = None
622 if 'ZUUL_CHANGE_IDS' in self.parameters:
623 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700624
James E. Blair3158e282016-08-19 09:34:11 -0700625 def __repr__(self):
626 waiting = ''
627 if self.waiting:
628 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100629 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
630 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700631
Clark Boylanb640e052014-04-03 16:41:46 -0700632 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700633 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700634 self.wait_condition.acquire()
635 self.wait_condition.notify()
636 self.waiting = False
637 self.log.debug("Build %s released" % self.unique)
638 self.wait_condition.release()
639
640 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700641 """Return whether this build is being held.
642
643 :returns: Whether the build is being held.
644 :rtype: bool
645 """
646
Clark Boylanb640e052014-04-03 16:41:46 -0700647 self.wait_condition.acquire()
648 if self.waiting:
649 ret = True
650 else:
651 ret = False
652 self.wait_condition.release()
653 return ret
654
655 def _wait(self):
656 self.wait_condition.acquire()
657 self.waiting = True
658 self.log.debug("Build %s waiting" % self.unique)
659 self.wait_condition.wait()
660 self.wait_condition.release()
661
662 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700663 self.log.debug('Running build %s' % self.unique)
664
Paul Belanger174a8272017-03-14 13:20:10 -0400665 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700666 self.log.debug('Holding build %s' % self.unique)
667 self._wait()
668 self.log.debug("Build %s continuing" % self.unique)
669
James E. Blair412fba82017-01-26 15:00:50 -0800670 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700671 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800672 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700673 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800674 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500675 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800676 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700677
James E. Blaire1767bc2016-08-02 10:00:27 -0700678 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700679
James E. Blaira5dba232016-08-08 15:53:24 -0700680 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400681 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700682 for change in changes:
683 if self.hasChanges(change):
684 return True
685 return False
686
James E. Blaire7b99a02016-08-05 14:27:34 -0700687 def hasChanges(self, *changes):
688 """Return whether this build has certain changes in its git repos.
689
690 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700691 are expected to be present (in order) in the git repository of
692 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700693
694 :returns: Whether the build has the indicated changes.
695 :rtype: bool
696
697 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800698 for change in changes:
Monty Taylord642d852017-02-23 14:05:42 -0500699 path = os.path.join(self.jobdir.src_root, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800700 try:
701 repo = git.Repo(path)
702 except NoSuchPathError as e:
703 self.log.debug('%s' % e)
704 return False
705 ref = self.parameters['ZUUL_REF']
706 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
707 commit_message = '%s-1' % change.subject
708 self.log.debug("Checking if build %s has changes; commit_message "
709 "%s; repo_messages %s" % (self, commit_message,
710 repo_messages))
711 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700712 self.log.debug(" messages do not match")
713 return False
714 self.log.debug(" OK")
715 return True
716
Clark Boylanb640e052014-04-03 16:41:46 -0700717
Paul Belanger174a8272017-03-14 13:20:10 -0400718class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
719 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700720
Paul Belanger174a8272017-03-14 13:20:10 -0400721 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700722 they will report that they have started but then pause until
723 released before reporting completion. This attribute may be
724 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400725 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700726 be explicitly released.
727
728 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800729 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700730 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800731 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400732 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700733 self.hold_jobs_in_build = False
734 self.lock = threading.Lock()
735 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700736 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700737 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700738 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800739
James E. Blaira5dba232016-08-08 15:53:24 -0700740 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400741 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700742
743 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700744 :arg Change change: The :py:class:`~tests.base.FakeChange`
745 instance which should cause the job to fail. This job
746 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700747
748 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700749 l = self.fail_tests.get(name, [])
750 l.append(change)
751 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800752
James E. Blair962220f2016-08-03 11:22:38 -0700753 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700754 """Release a held build.
755
756 :arg str regex: A regular expression which, if supplied, will
757 cause only builds with matching names to be released. If
758 not supplied, all builds will be released.
759
760 """
James E. Blair962220f2016-08-03 11:22:38 -0700761 builds = self.running_builds[:]
762 self.log.debug("Releasing build %s (%s)" % (regex,
763 len(self.running_builds)))
764 for build in builds:
765 if not regex or re.match(regex, build.name):
766 self.log.debug("Releasing build %s" %
767 (build.parameters['ZUUL_UUID']))
768 build.release()
769 else:
770 self.log.debug("Not releasing build %s" %
771 (build.parameters['ZUUL_UUID']))
772 self.log.debug("Done releasing builds %s (%s)" %
773 (regex, len(self.running_builds)))
774
Paul Belanger174a8272017-03-14 13:20:10 -0400775 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700776 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700777 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700778 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700779 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800780 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500781 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800782 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100783 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
784 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700785
786 def stopJob(self, job):
787 self.log.debug("handle stop")
788 parameters = json.loads(job.arguments)
789 uuid = parameters['uuid']
790 for build in self.running_builds:
791 if build.unique == uuid:
792 build.aborted = True
793 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400794 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700795
James E. Blaira002b032017-04-18 10:35:48 -0700796 def stop(self):
797 for build in self.running_builds:
798 build.release()
799 super(RecordingExecutorServer, self).stop()
800
Joshua Hesketh50c21782016-10-13 21:34:14 +1100801
Paul Belanger174a8272017-03-14 13:20:10 -0400802class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700803 def doMergeChanges(self, items):
804 # Get a merger in order to update the repos involved in this job.
805 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
806 if not commit: # merge conflict
807 self.recordResult('MERGER_FAILURE')
808 return commit
809
810 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400811 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400812 self.executor_server.lock.acquire()
813 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700814 BuildHistory(name=build.name, result=result, changes=build.changes,
815 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800816 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700817 pipeline=build.parameters['ZUUL_PIPELINE'])
818 )
Paul Belanger174a8272017-03-14 13:20:10 -0400819 self.executor_server.running_builds.remove(build)
820 del self.executor_server.job_builds[self.job.unique]
821 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700822
823 def runPlaybooks(self, args):
824 build = self.executor_server.job_builds[self.job.unique]
825 build.jobdir = self.jobdir
826
827 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
828 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800829 return result
830
Monty Taylore6562aa2017-02-20 07:37:39 -0500831 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400832 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800833
Paul Belanger174a8272017-03-14 13:20:10 -0400834 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600835 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -0500836 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -0800837 else:
838 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -0700839 return result
James E. Blairf5dbd002015-12-23 15:26:17 -0800840
James E. Blairad8dca02017-02-21 11:48:32 -0500841 def getHostList(self, args):
842 self.log.debug("hostlist")
843 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -0400844 for host in hosts:
845 host['host_vars']['ansible_connection'] = 'local'
846
847 hosts.append(dict(
848 name='localhost',
849 host_vars=dict(ansible_connection='local'),
850 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -0500851 return hosts
852
James E. Blairf5dbd002015-12-23 15:26:17 -0800853
Clark Boylanb640e052014-04-03 16:41:46 -0700854class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -0700855 """A Gearman server for use in tests.
856
857 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
858 added to the queue but will not be distributed to workers
859 until released. This attribute may be changed at any time and
860 will take effect for subsequently enqueued jobs, but
861 previously held jobs will still need to be explicitly
862 released.
863
864 """
865
Clark Boylanb640e052014-04-03 16:41:46 -0700866 def __init__(self):
867 self.hold_jobs_in_queue = False
868 super(FakeGearmanServer, self).__init__(0)
869
870 def getJobForConnection(self, connection, peek=False):
871 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
872 for job in queue:
873 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -0400874 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -0700875 job.waiting = self.hold_jobs_in_queue
876 else:
877 job.waiting = False
878 if job.waiting:
879 continue
880 if job.name in connection.functions:
881 if not peek:
882 queue.remove(job)
883 connection.related_jobs[job.handle] = job
884 job.worker_connection = connection
885 job.running = True
886 return job
887 return None
888
889 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700890 """Release a held job.
891
892 :arg str regex: A regular expression which, if supplied, will
893 cause only jobs with matching names to be released. If
894 not supplied, all jobs will be released.
895 """
Clark Boylanb640e052014-04-03 16:41:46 -0700896 released = False
897 qlen = (len(self.high_queue) + len(self.normal_queue) +
898 len(self.low_queue))
899 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
900 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -0400901 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -0700902 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -0500903 parameters = json.loads(job.arguments)
904 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -0700905 self.log.debug("releasing queued job %s" %
906 job.unique)
907 job.waiting = False
908 released = True
909 else:
910 self.log.debug("not releasing queued job %s" %
911 job.unique)
912 if released:
913 self.wakeConnections()
914 qlen = (len(self.high_queue) + len(self.normal_queue) +
915 len(self.low_queue))
916 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
917
918
919class FakeSMTP(object):
920 log = logging.getLogger('zuul.FakeSMTP')
921
922 def __init__(self, messages, server, port):
923 self.server = server
924 self.port = port
925 self.messages = messages
926
927 def sendmail(self, from_email, to_email, msg):
928 self.log.info("Sending email from %s, to %s, with msg %s" % (
929 from_email, to_email, msg))
930
931 headers = msg.split('\n\n', 1)[0]
932 body = msg.split('\n\n', 1)[1]
933
934 self.messages.append(dict(
935 from_email=from_email,
936 to_email=to_email,
937 msg=msg,
938 headers=headers,
939 body=body,
940 ))
941
942 return True
943
944 def quit(self):
945 return True
946
947
James E. Blairdce6cea2016-12-20 16:45:32 -0800948class FakeNodepool(object):
949 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -0800950 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -0800951
952 log = logging.getLogger("zuul.test.FakeNodepool")
953
954 def __init__(self, host, port, chroot):
955 self.client = kazoo.client.KazooClient(
956 hosts='%s:%s%s' % (host, port, chroot))
957 self.client.start()
958 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -0800959 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -0800960 self.thread = threading.Thread(target=self.run)
961 self.thread.daemon = True
962 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -0800963 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -0800964
965 def stop(self):
966 self._running = False
967 self.thread.join()
968 self.client.stop()
969 self.client.close()
970
971 def run(self):
972 while self._running:
973 self._run()
974 time.sleep(0.1)
975
976 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -0800977 if self.paused:
978 return
James E. Blairdce6cea2016-12-20 16:45:32 -0800979 for req in self.getNodeRequests():
980 self.fulfillRequest(req)
981
982 def getNodeRequests(self):
983 try:
984 reqids = self.client.get_children(self.REQUEST_ROOT)
985 except kazoo.exceptions.NoNodeError:
986 return []
987 reqs = []
988 for oid in sorted(reqids):
989 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -0800990 try:
991 data, stat = self.client.get(path)
992 data = json.loads(data)
993 data['_oid'] = oid
994 reqs.append(data)
995 except kazoo.exceptions.NoNodeError:
996 pass
James E. Blairdce6cea2016-12-20 16:45:32 -0800997 return reqs
998
James E. Blaire18d4602017-01-05 11:17:28 -0800999 def getNodes(self):
1000 try:
1001 nodeids = self.client.get_children(self.NODE_ROOT)
1002 except kazoo.exceptions.NoNodeError:
1003 return []
1004 nodes = []
1005 for oid in sorted(nodeids):
1006 path = self.NODE_ROOT + '/' + oid
1007 data, stat = self.client.get(path)
1008 data = json.loads(data)
1009 data['_oid'] = oid
1010 try:
1011 lockfiles = self.client.get_children(path + '/lock')
1012 except kazoo.exceptions.NoNodeError:
1013 lockfiles = []
1014 if lockfiles:
1015 data['_lock'] = True
1016 else:
1017 data['_lock'] = False
1018 nodes.append(data)
1019 return nodes
1020
James E. Blaira38c28e2017-01-04 10:33:20 -08001021 def makeNode(self, request_id, node_type):
1022 now = time.time()
1023 path = '/nodepool/nodes/'
1024 data = dict(type=node_type,
1025 provider='test-provider',
1026 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001027 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001028 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001029 public_ipv4='127.0.0.1',
1030 private_ipv4=None,
1031 public_ipv6=None,
1032 allocated_to=request_id,
1033 state='ready',
1034 state_time=now,
1035 created_time=now,
1036 updated_time=now,
1037 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001038 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001039 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001040 data = json.dumps(data)
1041 path = self.client.create(path, data,
1042 makepath=True,
1043 sequence=True)
1044 nodeid = path.split("/")[-1]
1045 return nodeid
1046
James E. Blair6ab79e02017-01-06 10:10:17 -08001047 def addFailRequest(self, request):
1048 self.fail_requests.add(request['_oid'])
1049
James E. Blairdce6cea2016-12-20 16:45:32 -08001050 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001051 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001052 return
1053 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001054 oid = request['_oid']
1055 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001056
James E. Blair6ab79e02017-01-06 10:10:17 -08001057 if oid in self.fail_requests:
1058 request['state'] = 'failed'
1059 else:
1060 request['state'] = 'fulfilled'
1061 nodes = []
1062 for node in request['node_types']:
1063 nodeid = self.makeNode(oid, node)
1064 nodes.append(nodeid)
1065 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001066
James E. Blaira38c28e2017-01-04 10:33:20 -08001067 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001068 path = self.REQUEST_ROOT + '/' + oid
1069 data = json.dumps(request)
1070 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1071 self.client.set(path, data)
1072
1073
James E. Blair498059b2016-12-20 13:50:13 -08001074class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001075 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001076 super(ChrootedKazooFixture, self).__init__()
1077
1078 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1079 if ':' in zk_host:
1080 host, port = zk_host.split(':')
1081 else:
1082 host = zk_host
1083 port = None
1084
1085 self.zookeeper_host = host
1086
1087 if not port:
1088 self.zookeeper_port = 2181
1089 else:
1090 self.zookeeper_port = int(port)
1091
Clark Boylan621ec9a2017-04-07 17:41:33 -07001092 self.test_id = test_id
1093
James E. Blair498059b2016-12-20 13:50:13 -08001094 def _setUp(self):
1095 # Make sure the test chroot paths do not conflict
1096 random_bits = ''.join(random.choice(string.ascii_lowercase +
1097 string.ascii_uppercase)
1098 for x in range(8))
1099
Clark Boylan621ec9a2017-04-07 17:41:33 -07001100 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001101 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1102
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001103 self.addCleanup(self._cleanup)
1104
James E. Blair498059b2016-12-20 13:50:13 -08001105 # Ensure the chroot path exists and clean up any pre-existing znodes.
1106 _tmp_client = kazoo.client.KazooClient(
1107 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1108 _tmp_client.start()
1109
1110 if _tmp_client.exists(self.zookeeper_chroot):
1111 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1112
1113 _tmp_client.ensure_path(self.zookeeper_chroot)
1114 _tmp_client.stop()
1115 _tmp_client.close()
1116
James E. Blair498059b2016-12-20 13:50:13 -08001117 def _cleanup(self):
1118 '''Remove the chroot path.'''
1119 # Need a non-chroot'ed client to remove the chroot path
1120 _tmp_client = kazoo.client.KazooClient(
1121 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1122 _tmp_client.start()
1123 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1124 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001125 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001126
1127
Joshua Heskethd78b4482015-09-14 16:56:34 -06001128class MySQLSchemaFixture(fixtures.Fixture):
1129 def setUp(self):
1130 super(MySQLSchemaFixture, self).setUp()
1131
1132 random_bits = ''.join(random.choice(string.ascii_lowercase +
1133 string.ascii_uppercase)
1134 for x in range(8))
1135 self.name = '%s_%s' % (random_bits, os.getpid())
1136 self.passwd = uuid.uuid4().hex
1137 db = pymysql.connect(host="localhost",
1138 user="openstack_citest",
1139 passwd="openstack_citest",
1140 db="openstack_citest")
1141 cur = db.cursor()
1142 cur.execute("create database %s" % self.name)
1143 cur.execute(
1144 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1145 (self.name, self.name, self.passwd))
1146 cur.execute("flush privileges")
1147
1148 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1149 self.passwd,
1150 self.name)
1151 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1152 self.addCleanup(self.cleanup)
1153
1154 def cleanup(self):
1155 db = pymysql.connect(host="localhost",
1156 user="openstack_citest",
1157 passwd="openstack_citest",
1158 db="openstack_citest")
1159 cur = db.cursor()
1160 cur.execute("drop database %s" % self.name)
1161 cur.execute("drop user '%s'@'localhost'" % self.name)
1162 cur.execute("flush privileges")
1163
1164
Maru Newby3fe5f852015-01-13 04:22:14 +00001165class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001166 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001167 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001168
James E. Blair1c236df2017-02-01 14:07:24 -08001169 def attachLogs(self, *args):
1170 def reader():
1171 self._log_stream.seek(0)
1172 while True:
1173 x = self._log_stream.read(4096)
1174 if not x:
1175 break
1176 yield x.encode('utf8')
1177 content = testtools.content.content_from_reader(
1178 reader,
1179 testtools.content_type.UTF8_TEXT,
1180 False)
1181 self.addDetail('logging', content)
1182
Clark Boylanb640e052014-04-03 16:41:46 -07001183 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001184 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001185 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1186 try:
1187 test_timeout = int(test_timeout)
1188 except ValueError:
1189 # If timeout value is invalid do not set a timeout.
1190 test_timeout = 0
1191 if test_timeout > 0:
1192 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1193
1194 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1195 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1196 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1197 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1198 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1199 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1200 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1201 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1202 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1203 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001204 self._log_stream = StringIO()
1205 self.addOnException(self.attachLogs)
1206 else:
1207 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001208
James E. Blair1c236df2017-02-01 14:07:24 -08001209 handler = logging.StreamHandler(self._log_stream)
1210 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1211 '%(levelname)-8s %(message)s')
1212 handler.setFormatter(formatter)
1213
1214 logger = logging.getLogger()
1215 logger.setLevel(logging.DEBUG)
1216 logger.addHandler(handler)
1217
1218 # NOTE(notmorgan): Extract logging overrides for specific
1219 # libraries from the OS_LOG_DEFAULTS env and create loggers
1220 # for each. This is used to limit the output during test runs
1221 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001222 log_defaults_from_env = os.environ.get(
1223 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001224 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001225
James E. Blairdce6cea2016-12-20 16:45:32 -08001226 if log_defaults_from_env:
1227 for default in log_defaults_from_env.split(','):
1228 try:
1229 name, level_str = default.split('=', 1)
1230 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001231 logger = logging.getLogger(name)
1232 logger.setLevel(level)
1233 logger.addHandler(handler)
1234 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001235 except ValueError:
1236 # NOTE(notmorgan): Invalid format of the log default,
1237 # skip and don't try and apply a logger for the
1238 # specified module
1239 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001240
Maru Newby3fe5f852015-01-13 04:22:14 +00001241
1242class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001243 """A test case with a functioning Zuul.
1244
1245 The following class variables are used during test setup and can
1246 be overidden by subclasses but are effectively read-only once a
1247 test method starts running:
1248
1249 :cvar str config_file: This points to the main zuul config file
1250 within the fixtures directory. Subclasses may override this
1251 to obtain a different behavior.
1252
1253 :cvar str tenant_config_file: This is the tenant config file
1254 (which specifies from what git repos the configuration should
1255 be loaded). It defaults to the value specified in
1256 `config_file` but can be overidden by subclasses to obtain a
1257 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001258 configuration. See also the :py:func:`simple_layout`
1259 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001260
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001261 :cvar bool create_project_keys: Indicates whether Zuul should
1262 auto-generate keys for each project, or whether the test
1263 infrastructure should insert dummy keys to save time during
1264 startup. Defaults to False.
1265
James E. Blaire7b99a02016-08-05 14:27:34 -07001266 The following are instance variables that are useful within test
1267 methods:
1268
1269 :ivar FakeGerritConnection fake_<connection>:
1270 A :py:class:`~tests.base.FakeGerritConnection` will be
1271 instantiated for each connection present in the config file
1272 and stored here. For instance, `fake_gerrit` will hold the
1273 FakeGerritConnection object for a connection named `gerrit`.
1274
1275 :ivar FakeGearmanServer gearman_server: An instance of
1276 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1277 server that all of the Zuul components in this test use to
1278 communicate with each other.
1279
Paul Belanger174a8272017-03-14 13:20:10 -04001280 :ivar RecordingExecutorServer executor_server: An instance of
1281 :py:class:`~tests.base.RecordingExecutorServer` which is the
1282 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001283
1284 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1285 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001286 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001287 list upon completion.
1288
1289 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1290 objects representing completed builds. They are appended to
1291 the list in the order they complete.
1292
1293 """
1294
James E. Blair83005782015-12-11 14:46:03 -08001295 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001296 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001297 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001298
1299 def _startMerger(self):
1300 self.merge_server = zuul.merger.server.MergeServer(self.config,
1301 self.connections)
1302 self.merge_server.start()
1303
Maru Newby3fe5f852015-01-13 04:22:14 +00001304 def setUp(self):
1305 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001306
1307 self.setupZK()
1308
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001309 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001310 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001311 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1312 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001313 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001314 tmp_root = tempfile.mkdtemp(
1315 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001316 self.test_root = os.path.join(tmp_root, "zuul-test")
1317 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001318 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001319 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001320 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001321
1322 if os.path.exists(self.test_root):
1323 shutil.rmtree(self.test_root)
1324 os.makedirs(self.test_root)
1325 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001326 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001327
1328 # Make per test copy of Configuration.
1329 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001330 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001331 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001332 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001333 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001334 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001335 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001336
Clark Boylanb640e052014-04-03 16:41:46 -07001337 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001338 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1339 # see: https://github.com/jsocol/pystatsd/issues/61
1340 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001341 os.environ['STATSD_PORT'] = str(self.statsd.port)
1342 self.statsd.start()
1343 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001344 reload_module(statsd)
1345 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001346
1347 self.gearman_server = FakeGearmanServer()
1348
1349 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001350 self.log.info("Gearman server on port %s" %
1351 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001352
James E. Blaire511d2f2016-12-08 15:22:26 -08001353 gerritsource.GerritSource.replication_timeout = 1.5
1354 gerritsource.GerritSource.replication_retry_interval = 0.5
1355 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001356
Joshua Hesketh352264b2015-08-11 23:42:08 +10001357 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001358
Jan Hruban6b71aff2015-10-22 16:58:08 +02001359 self.event_queues = [
1360 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001361 self.sched.trigger_event_queue,
1362 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001363 ]
1364
James E. Blairfef78942016-03-11 16:28:56 -08001365 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001366 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001367
Clark Boylanb640e052014-04-03 16:41:46 -07001368 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001369 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001370 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001371 return FakeURLOpener(self.upstream_root, *args, **kw)
1372
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001373 old_urlopen = urllib.request.urlopen
1374 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001375
James E. Blair3f876d52016-07-22 13:07:14 -07001376 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001377
Paul Belanger174a8272017-03-14 13:20:10 -04001378 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001379 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001380 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001381 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001382 _test_root=self.test_root,
1383 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001384 self.executor_server.start()
1385 self.history = self.executor_server.build_history
1386 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001387
Paul Belanger174a8272017-03-14 13:20:10 -04001388 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001389 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001390 self.merge_client = zuul.merger.client.MergeClient(
1391 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001392 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001393 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001394 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001395
James E. Blair0d5a36e2017-02-21 10:53:44 -05001396 self.fake_nodepool = FakeNodepool(
1397 self.zk_chroot_fixture.zookeeper_host,
1398 self.zk_chroot_fixture.zookeeper_port,
1399 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001400
Paul Belanger174a8272017-03-14 13:20:10 -04001401 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001402 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001403 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001404 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001405
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001406 self.webapp = zuul.webapp.WebApp(
1407 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001408 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001409
1410 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001411 self.webapp.start()
1412 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001413 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001414 # Cleanups are run in reverse order
1415 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001417 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001418
James E. Blairb9c0d772017-03-03 14:34:49 -08001419 self.sched.reconfigure(self.config)
1420 self.sched.resume()
1421
James E. Blairfef78942016-03-11 16:28:56 -08001422 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001423 # Set up gerrit related fakes
1424 # Set a changes database so multiple FakeGerrit's can report back to
1425 # a virtual canonical database given by the configured hostname
1426 self.gerrit_changes_dbs = {}
1427
1428 def getGerritConnection(driver, name, config):
1429 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1430 con = FakeGerritConnection(driver, name, config,
1431 changes_db=db,
1432 upstream_root=self.upstream_root)
1433 self.event_queues.append(con.event_queue)
1434 setattr(self, 'fake_' + name, con)
1435 return con
1436
1437 self.useFixture(fixtures.MonkeyPatch(
1438 'zuul.driver.gerrit.GerritDriver.getConnection',
1439 getGerritConnection))
1440
1441 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001442 # TODO(jhesketh): This should come from lib.connections for better
1443 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001444 # Register connections from the config
1445 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001446
Joshua Hesketh352264b2015-08-11 23:42:08 +10001447 def FakeSMTPFactory(*args, **kw):
1448 args = [self.smtp_messages] + list(args)
1449 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001450
Joshua Hesketh352264b2015-08-11 23:42:08 +10001451 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001452
James E. Blaire511d2f2016-12-08 15:22:26 -08001453 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001454 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001455 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001456
James E. Blair83005782015-12-11 14:46:03 -08001457 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001458 # This creates the per-test configuration object. It can be
1459 # overriden by subclasses, but should not need to be since it
1460 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001461 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001462 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001463
1464 if not self.setupSimpleLayout():
1465 if hasattr(self, 'tenant_config_file'):
1466 self.config.set('zuul', 'tenant_config',
1467 self.tenant_config_file)
1468 git_path = os.path.join(
1469 os.path.dirname(
1470 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1471 'git')
1472 if os.path.exists(git_path):
1473 for reponame in os.listdir(git_path):
1474 project = reponame.replace('_', '/')
1475 self.copyDirToRepo(project,
1476 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001477 self.setupAllProjectKeys()
1478
James E. Blair06cc3922017-04-19 10:08:10 -07001479 def setupSimpleLayout(self):
1480 # If the test method has been decorated with a simple_layout,
1481 # use that instead of the class tenant_config_file. Set up a
1482 # single config-project with the specified layout, and
1483 # initialize repos for all of the 'project' entries which
1484 # appear in the layout.
1485 test_name = self.id().split('.')[-1]
1486 test = getattr(self, test_name)
1487 if hasattr(test, '__simple_layout__'):
1488 path = getattr(test, '__simple_layout__')
1489 else:
1490 return False
1491
James E. Blairb70e55a2017-04-19 12:57:02 -07001492 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001493 path = os.path.join(FIXTURE_DIR, path)
1494 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001495 data = f.read()
1496 layout = yaml.safe_load(data)
1497 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001498 untrusted_projects = []
1499 for item in layout:
1500 if 'project' in item:
1501 name = item['project']['name']
1502 untrusted_projects.append(name)
1503 self.init_repo(name)
1504 self.addCommitToRepo(name, 'initial commit',
1505 files={'README': ''},
1506 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001507 if 'job' in item:
1508 jobname = item['job']['name']
1509 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001510
1511 root = os.path.join(self.test_root, "config")
1512 if not os.path.exists(root):
1513 os.makedirs(root)
1514 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1515 config = [{'tenant':
1516 {'name': 'tenant-one',
1517 'source': {'gerrit':
1518 {'config-projects': ['common-config'],
1519 'untrusted-projects': untrusted_projects}}}}]
1520 f.write(yaml.dump(config))
1521 f.close()
1522 self.config.set('zuul', 'tenant_config',
1523 os.path.join(FIXTURE_DIR, f.name))
1524
1525 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001526 self.addCommitToRepo('common-config', 'add content from fixture',
1527 files, branch='master', tag='init')
1528
1529 return True
1530
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001531 def setupAllProjectKeys(self):
1532 if self.create_project_keys:
1533 return
1534
1535 path = self.config.get('zuul', 'tenant_config')
1536 with open(os.path.join(FIXTURE_DIR, path)) as f:
1537 tenant_config = yaml.safe_load(f.read())
1538 for tenant in tenant_config:
1539 sources = tenant['tenant']['source']
1540 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001541 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001542 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001543 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001544 self.setupProjectKeys(source, project)
1545
1546 def setupProjectKeys(self, source, project):
1547 # Make sure we set up an RSA key for the project so that we
1548 # don't spend time generating one:
1549
1550 key_root = os.path.join(self.state_root, 'keys')
1551 if not os.path.isdir(key_root):
1552 os.mkdir(key_root, 0o700)
1553 private_key_file = os.path.join(key_root, source, project + '.pem')
1554 private_key_dir = os.path.dirname(private_key_file)
1555 self.log.debug("Installing test keys for project %s at %s" % (
1556 project, private_key_file))
1557 if not os.path.isdir(private_key_dir):
1558 os.makedirs(private_key_dir)
1559 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1560 with open(private_key_file, 'w') as o:
1561 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001562
James E. Blair498059b2016-12-20 13:50:13 -08001563 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001564 self.zk_chroot_fixture = self.useFixture(
1565 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001566 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001567 self.zk_chroot_fixture.zookeeper_host,
1568 self.zk_chroot_fixture.zookeeper_port,
1569 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001570
James E. Blair96c6bf82016-01-15 16:20:40 -08001571 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001572 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001573
1574 files = {}
1575 for (dirpath, dirnames, filenames) in os.walk(source_path):
1576 for filename in filenames:
1577 test_tree_filepath = os.path.join(dirpath, filename)
1578 common_path = os.path.commonprefix([test_tree_filepath,
1579 source_path])
1580 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1581 with open(test_tree_filepath, 'r') as f:
1582 content = f.read()
1583 files[relative_filepath] = content
1584 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001585 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001586
James E. Blaire18d4602017-01-05 11:17:28 -08001587 def assertNodepoolState(self):
1588 # Make sure that there are no pending requests
1589
1590 requests = self.fake_nodepool.getNodeRequests()
1591 self.assertEqual(len(requests), 0)
1592
1593 nodes = self.fake_nodepool.getNodes()
1594 for node in nodes:
1595 self.assertFalse(node['_lock'], "Node %s is locked" %
1596 (node['_oid'],))
1597
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001598 def assertNoGeneratedKeys(self):
1599 # Make sure that Zuul did not generate any project keys
1600 # (unless it was supposed to).
1601
1602 if self.create_project_keys:
1603 return
1604
1605 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1606 test_key = i.read()
1607
1608 key_root = os.path.join(self.state_root, 'keys')
1609 for root, dirname, files in os.walk(key_root):
1610 for fn in files:
1611 with open(os.path.join(root, fn)) as f:
1612 self.assertEqual(test_key, f.read())
1613
Clark Boylanb640e052014-04-03 16:41:46 -07001614 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001615 self.log.debug("Assert final state")
1616 # Make sure no jobs are running
1617 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001618 # Make sure that git.Repo objects have been garbage collected.
1619 repos = []
1620 gc.collect()
1621 for obj in gc.get_objects():
1622 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001623 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001624 repos.append(obj)
1625 self.assertEqual(len(repos), 0)
1626 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001627 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001628 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001629 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001630 for tenant in self.sched.abide.tenants.values():
1631 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001632 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001633 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001634
1635 def shutdown(self):
1636 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001637 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001638 self.merge_server.stop()
1639 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001640 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001641 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001642 self.sched.stop()
1643 self.sched.join()
1644 self.statsd.stop()
1645 self.statsd.join()
1646 self.webapp.stop()
1647 self.webapp.join()
1648 self.rpc.stop()
1649 self.rpc.join()
1650 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001651 self.fake_nodepool.stop()
1652 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001653 threads = threading.enumerate()
1654 if len(threads) > 1:
1655 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001656 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001657
James E. Blaira002b032017-04-18 10:35:48 -07001658 def assertCleanShutdown(self):
1659 pass
1660
James E. Blairc4ba97a2017-04-19 16:26:24 -07001661 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001662 parts = project.split('/')
1663 path = os.path.join(self.upstream_root, *parts[:-1])
1664 if not os.path.exists(path):
1665 os.makedirs(path)
1666 path = os.path.join(self.upstream_root, project)
1667 repo = git.Repo.init(path)
1668
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001669 with repo.config_writer() as config_writer:
1670 config_writer.set_value('user', 'email', 'user@example.com')
1671 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001672
Clark Boylanb640e052014-04-03 16:41:46 -07001673 repo.index.commit('initial commit')
1674 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001675 if tag:
1676 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001677
James E. Blair97d902e2014-08-21 13:25:56 -07001678 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001679 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001680 repo.git.clean('-x', '-f', '-d')
1681
James E. Blair97d902e2014-08-21 13:25:56 -07001682 def create_branch(self, project, branch):
1683 path = os.path.join(self.upstream_root, project)
1684 repo = git.Repo.init(path)
1685 fn = os.path.join(path, 'README')
1686
1687 branch_head = repo.create_head(branch)
1688 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001689 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001690 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001691 f.close()
1692 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001693 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001694
James E. Blair97d902e2014-08-21 13:25:56 -07001695 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001696 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001697 repo.git.clean('-x', '-f', '-d')
1698
Sachi King9f16d522016-03-16 12:20:45 +11001699 def create_commit(self, project):
1700 path = os.path.join(self.upstream_root, project)
1701 repo = git.Repo(path)
1702 repo.head.reference = repo.heads['master']
1703 file_name = os.path.join(path, 'README')
1704 with open(file_name, 'a') as f:
1705 f.write('creating fake commit\n')
1706 repo.index.add([file_name])
1707 commit = repo.index.commit('Creating a fake commit')
1708 return commit.hexsha
1709
James E. Blairf4a5f022017-04-18 14:01:10 -07001710 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001711 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001712 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001713 while len(self.builds):
1714 self.release(self.builds[0])
1715 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001716 i += 1
1717 if count is not None and i >= count:
1718 break
James E. Blairb8c16472015-05-05 14:55:26 -07001719
Clark Boylanb640e052014-04-03 16:41:46 -07001720 def release(self, job):
1721 if isinstance(job, FakeBuild):
1722 job.release()
1723 else:
1724 job.waiting = False
1725 self.log.debug("Queued job %s released" % job.unique)
1726 self.gearman_server.wakeConnections()
1727
1728 def getParameter(self, job, name):
1729 if isinstance(job, FakeBuild):
1730 return job.parameters[name]
1731 else:
1732 parameters = json.loads(job.arguments)
1733 return parameters[name]
1734
Clark Boylanb640e052014-04-03 16:41:46 -07001735 def haveAllBuildsReported(self):
1736 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001737 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001738 return False
1739 # Find out if every build that the worker has completed has been
1740 # reported back to Zuul. If it hasn't then that means a Gearman
1741 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001742 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001743 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001744 if not zbuild:
1745 # It has already been reported
1746 continue
1747 # It hasn't been reported yet.
1748 return False
1749 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001750 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001751 if connection.state == 'GRAB_WAIT':
1752 return False
1753 return True
1754
1755 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001756 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001757 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001758 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001759 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001760 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001761 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001762 for j in conn.related_jobs.values():
1763 if j.unique == build.uuid:
1764 client_job = j
1765 break
1766 if not client_job:
1767 self.log.debug("%s is not known to the gearman client" %
1768 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001769 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001770 if not client_job.handle:
1771 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001772 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001773 server_job = self.gearman_server.jobs.get(client_job.handle)
1774 if not server_job:
1775 self.log.debug("%s is not known to the gearman server" %
1776 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001777 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001778 if not hasattr(server_job, 'waiting'):
1779 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001780 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001781 if server_job.waiting:
1782 continue
James E. Blair17302972016-08-10 16:11:42 -07001783 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001784 self.log.debug("%s has not reported start" % build)
1785 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001786 worker_build = self.executor_server.job_builds.get(
1787 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001788 if worker_build:
1789 if worker_build.isWaiting():
1790 continue
1791 else:
1792 self.log.debug("%s is running" % worker_build)
1793 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001794 else:
James E. Blair962220f2016-08-03 11:22:38 -07001795 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001796 return False
James E. Blaira002b032017-04-18 10:35:48 -07001797 for (build_uuid, job_worker) in \
1798 self.executor_server.job_workers.items():
1799 if build_uuid not in seen_builds:
1800 self.log.debug("%s is not finalized" % build_uuid)
1801 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001802 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001803
James E. Blairdce6cea2016-12-20 16:45:32 -08001804 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001805 if self.fake_nodepool.paused:
1806 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 if self.sched.nodepool.requests:
1808 return False
1809 return True
1810
Jan Hruban6b71aff2015-10-22 16:58:08 +02001811 def eventQueuesEmpty(self):
1812 for queue in self.event_queues:
1813 yield queue.empty()
1814
1815 def eventQueuesJoin(self):
1816 for queue in self.event_queues:
1817 queue.join()
1818
Clark Boylanb640e052014-04-03 16:41:46 -07001819 def waitUntilSettled(self):
1820 self.log.debug("Waiting until settled...")
1821 start = time.time()
1822 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001823 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001824 self.log.error("Timeout waiting for Zuul to settle")
1825 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001826 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001827 self.log.error(" %s: %s" % (queue, queue.empty()))
1828 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001829 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001830 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001831 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001832 self.log.error("All requests completed: %s" %
1833 (self.areAllNodeRequestsComplete(),))
1834 self.log.error("Merge client jobs: %s" %
1835 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001836 raise Exception("Timeout waiting for Zuul to settle")
1837 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001838
Paul Belanger174a8272017-03-14 13:20:10 -04001839 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001840 # have all build states propogated to zuul?
1841 if self.haveAllBuildsReported():
1842 # Join ensures that the queue is empty _and_ events have been
1843 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001844 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001845 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001846 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001847 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001848 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001849 self.areAllNodeRequestsComplete() and
1850 all(self.eventQueuesEmpty())):
1851 # The queue empty check is placed at the end to
1852 # ensure that if a component adds an event between
1853 # when locked the run handler and checked that the
1854 # components were stable, we don't erroneously
1855 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001856 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001857 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001858 self.log.debug("...settled.")
1859 return
1860 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001861 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001862 self.sched.wake_event.wait(0.1)
1863
1864 def countJobResults(self, jobs, result):
1865 jobs = filter(lambda x: x.result == result, jobs)
1866 return len(jobs)
1867
James E. Blair96c6bf82016-01-15 16:20:40 -08001868 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001869 for job in self.history:
1870 if (job.name == name and
1871 (project is None or
1872 job.parameters['ZUUL_PROJECT'] == project)):
1873 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001874 raise Exception("Unable to find job %s in history" % name)
1875
1876 def assertEmptyQueues(self):
1877 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001878 for tenant in self.sched.abide.tenants.values():
1879 for pipeline in tenant.layout.pipelines.values():
1880 for queue in pipeline.queues:
1881 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001882 print('pipeline %s queue %s contents %s' % (
1883 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001884 self.assertEqual(len(queue.queue), 0,
1885 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001886
1887 def assertReportedStat(self, key, value=None, kind=None):
1888 start = time.time()
1889 while time.time() < (start + 5):
1890 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001891 k, v = stat.split(':')
1892 if key == k:
1893 if value is None and kind is None:
1894 return
1895 elif value:
1896 if value == v:
1897 return
1898 elif kind:
1899 if v.endswith('|' + kind):
1900 return
1901 time.sleep(0.1)
1902
Clark Boylanb640e052014-04-03 16:41:46 -07001903 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001904
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001905 def assertBuilds(self, builds):
1906 """Assert that the running builds are as described.
1907
1908 The list of running builds is examined and must match exactly
1909 the list of builds described by the input.
1910
1911 :arg list builds: A list of dictionaries. Each item in the
1912 list must match the corresponding build in the build
1913 history, and each element of the dictionary must match the
1914 corresponding attribute of the build.
1915
1916 """
James E. Blair3158e282016-08-19 09:34:11 -07001917 try:
1918 self.assertEqual(len(self.builds), len(builds))
1919 for i, d in enumerate(builds):
1920 for k, v in d.items():
1921 self.assertEqual(
1922 getattr(self.builds[i], k), v,
1923 "Element %i in builds does not match" % (i,))
1924 except Exception:
1925 for build in self.builds:
1926 self.log.error("Running build: %s" % build)
1927 else:
1928 self.log.error("No running builds")
1929 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001930
James E. Blairb536ecc2016-08-31 10:11:42 -07001931 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001932 """Assert that the completed builds are as described.
1933
1934 The list of completed builds is examined and must match
1935 exactly the list of builds described by the input.
1936
1937 :arg list history: A list of dictionaries. Each item in the
1938 list must match the corresponding build in the build
1939 history, and each element of the dictionary must match the
1940 corresponding attribute of the build.
1941
James E. Blairb536ecc2016-08-31 10:11:42 -07001942 :arg bool ordered: If true, the history must match the order
1943 supplied, if false, the builds are permitted to have
1944 arrived in any order.
1945
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001946 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001947 def matches(history_item, item):
1948 for k, v in item.items():
1949 if getattr(history_item, k) != v:
1950 return False
1951 return True
James E. Blair3158e282016-08-19 09:34:11 -07001952 try:
1953 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001954 if ordered:
1955 for i, d in enumerate(history):
1956 if not matches(self.history[i], d):
1957 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001958 "Element %i in history does not match %s" %
1959 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001960 else:
1961 unseen = self.history[:]
1962 for i, d in enumerate(history):
1963 found = False
1964 for unseen_item in unseen:
1965 if matches(unseen_item, d):
1966 found = True
1967 unseen.remove(unseen_item)
1968 break
1969 if not found:
1970 raise Exception("No match found for element %i "
1971 "in history" % (i,))
1972 if unseen:
1973 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001974 except Exception:
1975 for build in self.history:
1976 self.log.error("Completed build: %s" % build)
1977 else:
1978 self.log.error("No completed builds")
1979 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001980
James E. Blair6ac368c2016-12-22 18:07:20 -08001981 def printHistory(self):
1982 """Log the build history.
1983
1984 This can be useful during tests to summarize what jobs have
1985 completed.
1986
1987 """
1988 self.log.debug("Build history:")
1989 for build in self.history:
1990 self.log.debug(build)
1991
James E. Blair59fdbac2015-12-07 17:08:06 -08001992 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001993 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1994
James E. Blair9ea70072017-04-19 16:05:30 -07001995 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08001996 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08001997 if not os.path.exists(root):
1998 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08001999 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2000 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002001- tenant:
2002 name: openstack
2003 source:
2004 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002005 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002006 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002007 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002008 - org/project
2009 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002010 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002011 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002012 self.config.set('zuul', 'tenant_config',
2013 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002014 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002015
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002016 def addCommitToRepo(self, project, message, files,
2017 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002018 path = os.path.join(self.upstream_root, project)
2019 repo = git.Repo(path)
2020 repo.head.reference = branch
2021 zuul.merger.merger.reset_repo_to_head(repo)
2022 for fn, content in files.items():
2023 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002024 try:
2025 os.makedirs(os.path.dirname(fn))
2026 except OSError:
2027 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002028 with open(fn, 'w') as f:
2029 f.write(content)
2030 repo.index.add([fn])
2031 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002032 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002033 repo.heads[branch].commit = commit
2034 repo.head.reference = branch
2035 repo.git.clean('-x', '-f', '-d')
2036 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002037 if tag:
2038 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002039 return before
2040
2041 def commitLayoutUpdate(self, orig_name, source_name):
2042 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002043 source_name)
2044 to_copy = ['zuul.yaml']
2045 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2046 to_copy.append('playbooks/{}'.format(playbook))
2047 commit_data = {}
2048 for source_file in to_copy:
2049 source_file_path = os.path.join(source_path, source_file)
2050 with open(source_file_path, 'r') as nt:
2051 commit_data[source_file] = nt.read()
2052 before = self.addCommitToRepo(
2053 orig_name, 'Pulling content from %s' % source_name,
2054 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002055 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002056
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002057 def commitConfigUpdate(self, project_name, source_name):
2058 """Commit an update to zuul.yaml
2059
2060 This overwrites the zuul.yaml in the specificed project with
2061 the contents specified.
2062
2063 :arg str project_name: The name of the project containing
2064 zuul.yaml (e.g., common-config)
2065
2066 :arg str source_name: The path to the file (underneath the
2067 test fixture directory) whose contents should be used to
2068 replace zuul.yaml.
2069 """
2070
2071 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002072 files = {}
2073 with open(source_path, 'r') as f:
2074 data = f.read()
2075 layout = yaml.safe_load(data)
2076 files['zuul.yaml'] = data
2077 for item in layout:
2078 if 'job' in item:
2079 jobname = item['job']['name']
2080 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002081 before = self.addCommitToRepo(
2082 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002083 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002084 return before
2085
James E. Blair7fc8daa2016-08-08 15:37:15 -07002086 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002087
James E. Blair7fc8daa2016-08-08 15:37:15 -07002088 """Inject a Fake (Gerrit) event.
2089
2090 This method accepts a JSON-encoded event and simulates Zuul
2091 having received it from Gerrit. It could (and should)
2092 eventually apply to any connection type, but is currently only
2093 used with Gerrit connections. The name of the connection is
2094 used to look up the corresponding server, and the event is
2095 simulated as having been received by all Zuul connections
2096 attached to that server. So if two Gerrit connections in Zuul
2097 are connected to the same Gerrit server, and you invoke this
2098 method specifying the name of one of them, the event will be
2099 received by both.
2100
2101 .. note::
2102
2103 "self.fake_gerrit.addEvent" calls should be migrated to
2104 this method.
2105
2106 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002107 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002108 :arg str event: The JSON-encoded event.
2109
2110 """
2111 specified_conn = self.connections.connections[connection]
2112 for conn in self.connections.connections.values():
2113 if (isinstance(conn, specified_conn.__class__) and
2114 specified_conn.server == conn.server):
2115 conn.addEvent(event)
2116
James E. Blair3f876d52016-07-22 13:07:14 -07002117
2118class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002119 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002120 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002121
Joshua Heskethd78b4482015-09-14 16:56:34 -06002122
2123class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002124 def setup_config(self):
2125 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002126 for section_name in self.config.sections():
2127 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2128 section_name, re.I)
2129 if not con_match:
2130 continue
2131
2132 if self.config.get(section_name, 'driver') == 'sql':
2133 f = MySQLSchemaFixture()
2134 self.useFixture(f)
2135 if (self.config.get(section_name, 'dburi') ==
2136 '$MYSQL_FIXTURE_DBURI$'):
2137 self.config.set(section_name, 'dburi', f.dburi)