blob: d8c2fd6ca28a79d5bbb06a10fc7d7c113b6be1ea [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
1337 # For each project in config:
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001338 # TODOv3(jeblair): remove these and replace with new git
1339 # filesystem fixtures
Clark Boylanb640e052014-04-03 16:41:46 -07001340 self.init_repo("org/project3")
Clark Boylanb640e052014-04-03 16:41:46 -07001341
1342 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001343 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1344 # see: https://github.com/jsocol/pystatsd/issues/61
1345 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001346 os.environ['STATSD_PORT'] = str(self.statsd.port)
1347 self.statsd.start()
1348 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001349 reload_module(statsd)
1350 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001351
1352 self.gearman_server = FakeGearmanServer()
1353
1354 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001355 self.log.info("Gearman server on port %s" %
1356 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001357
James E. Blaire511d2f2016-12-08 15:22:26 -08001358 gerritsource.GerritSource.replication_timeout = 1.5
1359 gerritsource.GerritSource.replication_retry_interval = 0.5
1360 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001361
Joshua Hesketh352264b2015-08-11 23:42:08 +10001362 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001363
Jan Hruban6b71aff2015-10-22 16:58:08 +02001364 self.event_queues = [
1365 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001366 self.sched.trigger_event_queue,
1367 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001368 ]
1369
James E. Blairfef78942016-03-11 16:28:56 -08001370 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001371 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001372
Clark Boylanb640e052014-04-03 16:41:46 -07001373 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001374 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001375 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001376 return FakeURLOpener(self.upstream_root, *args, **kw)
1377
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001378 old_urlopen = urllib.request.urlopen
1379 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001380
James E. Blair3f876d52016-07-22 13:07:14 -07001381 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001382
Paul Belanger174a8272017-03-14 13:20:10 -04001383 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001384 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001385 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001386 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001387 _test_root=self.test_root,
1388 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001389 self.executor_server.start()
1390 self.history = self.executor_server.build_history
1391 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001392
Paul Belanger174a8272017-03-14 13:20:10 -04001393 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001394 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001395 self.merge_client = zuul.merger.client.MergeClient(
1396 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001397 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001398 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001399 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001400
James E. Blair0d5a36e2017-02-21 10:53:44 -05001401 self.fake_nodepool = FakeNodepool(
1402 self.zk_chroot_fixture.zookeeper_host,
1403 self.zk_chroot_fixture.zookeeper_port,
1404 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001405
Paul Belanger174a8272017-03-14 13:20:10 -04001406 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001408 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001409 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001410
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001411 self.webapp = zuul.webapp.WebApp(
1412 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001413 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001414
1415 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.webapp.start()
1417 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001418 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001419 # Cleanups are run in reverse order
1420 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001421 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001422 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001423
James E. Blairb9c0d772017-03-03 14:34:49 -08001424 self.sched.reconfigure(self.config)
1425 self.sched.resume()
1426
James E. Blairfef78942016-03-11 16:28:56 -08001427 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001428 # Set up gerrit related fakes
1429 # Set a changes database so multiple FakeGerrit's can report back to
1430 # a virtual canonical database given by the configured hostname
1431 self.gerrit_changes_dbs = {}
1432
1433 def getGerritConnection(driver, name, config):
1434 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1435 con = FakeGerritConnection(driver, name, config,
1436 changes_db=db,
1437 upstream_root=self.upstream_root)
1438 self.event_queues.append(con.event_queue)
1439 setattr(self, 'fake_' + name, con)
1440 return con
1441
1442 self.useFixture(fixtures.MonkeyPatch(
1443 'zuul.driver.gerrit.GerritDriver.getConnection',
1444 getGerritConnection))
1445
1446 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001447 # TODO(jhesketh): This should come from lib.connections for better
1448 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001449 # Register connections from the config
1450 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001451
Joshua Hesketh352264b2015-08-11 23:42:08 +10001452 def FakeSMTPFactory(*args, **kw):
1453 args = [self.smtp_messages] + list(args)
1454 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001455
Joshua Hesketh352264b2015-08-11 23:42:08 +10001456 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001457
James E. Blaire511d2f2016-12-08 15:22:26 -08001458 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001459 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001460 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001461
James E. Blair83005782015-12-11 14:46:03 -08001462 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001463 # This creates the per-test configuration object. It can be
1464 # overriden by subclasses, but should not need to be since it
1465 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001466 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001467 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001468
1469 if not self.setupSimpleLayout():
1470 if hasattr(self, 'tenant_config_file'):
1471 self.config.set('zuul', 'tenant_config',
1472 self.tenant_config_file)
1473 git_path = os.path.join(
1474 os.path.dirname(
1475 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1476 'git')
1477 if os.path.exists(git_path):
1478 for reponame in os.listdir(git_path):
1479 project = reponame.replace('_', '/')
1480 self.copyDirToRepo(project,
1481 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001482 self.setupAllProjectKeys()
1483
James E. Blair06cc3922017-04-19 10:08:10 -07001484 def setupSimpleLayout(self):
1485 # If the test method has been decorated with a simple_layout,
1486 # use that instead of the class tenant_config_file. Set up a
1487 # single config-project with the specified layout, and
1488 # initialize repos for all of the 'project' entries which
1489 # appear in the layout.
1490 test_name = self.id().split('.')[-1]
1491 test = getattr(self, test_name)
1492 if hasattr(test, '__simple_layout__'):
1493 path = getattr(test, '__simple_layout__')
1494 else:
1495 return False
1496
James E. Blairb70e55a2017-04-19 12:57:02 -07001497 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001498 path = os.path.join(FIXTURE_DIR, path)
1499 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001500 data = f.read()
1501 layout = yaml.safe_load(data)
1502 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001503 untrusted_projects = []
1504 for item in layout:
1505 if 'project' in item:
1506 name = item['project']['name']
1507 untrusted_projects.append(name)
1508 self.init_repo(name)
1509 self.addCommitToRepo(name, 'initial commit',
1510 files={'README': ''},
1511 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001512 if 'job' in item:
1513 jobname = item['job']['name']
1514 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001515
1516 root = os.path.join(self.test_root, "config")
1517 if not os.path.exists(root):
1518 os.makedirs(root)
1519 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1520 config = [{'tenant':
1521 {'name': 'tenant-one',
1522 'source': {'gerrit':
1523 {'config-projects': ['common-config'],
1524 'untrusted-projects': untrusted_projects}}}}]
1525 f.write(yaml.dump(config))
1526 f.close()
1527 self.config.set('zuul', 'tenant_config',
1528 os.path.join(FIXTURE_DIR, f.name))
1529
1530 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001531 self.addCommitToRepo('common-config', 'add content from fixture',
1532 files, branch='master', tag='init')
1533
1534 return True
1535
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001536 def setupAllProjectKeys(self):
1537 if self.create_project_keys:
1538 return
1539
1540 path = self.config.get('zuul', 'tenant_config')
1541 with open(os.path.join(FIXTURE_DIR, path)) as f:
1542 tenant_config = yaml.safe_load(f.read())
1543 for tenant in tenant_config:
1544 sources = tenant['tenant']['source']
1545 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001546 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001547 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001548 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001549 self.setupProjectKeys(source, project)
1550
1551 def setupProjectKeys(self, source, project):
1552 # Make sure we set up an RSA key for the project so that we
1553 # don't spend time generating one:
1554
1555 key_root = os.path.join(self.state_root, 'keys')
1556 if not os.path.isdir(key_root):
1557 os.mkdir(key_root, 0o700)
1558 private_key_file = os.path.join(key_root, source, project + '.pem')
1559 private_key_dir = os.path.dirname(private_key_file)
1560 self.log.debug("Installing test keys for project %s at %s" % (
1561 project, private_key_file))
1562 if not os.path.isdir(private_key_dir):
1563 os.makedirs(private_key_dir)
1564 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1565 with open(private_key_file, 'w') as o:
1566 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001567
James E. Blair498059b2016-12-20 13:50:13 -08001568 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001569 self.zk_chroot_fixture = self.useFixture(
1570 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001571 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001572 self.zk_chroot_fixture.zookeeper_host,
1573 self.zk_chroot_fixture.zookeeper_port,
1574 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001575
James E. Blair96c6bf82016-01-15 16:20:40 -08001576 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001577 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001578
1579 files = {}
1580 for (dirpath, dirnames, filenames) in os.walk(source_path):
1581 for filename in filenames:
1582 test_tree_filepath = os.path.join(dirpath, filename)
1583 common_path = os.path.commonprefix([test_tree_filepath,
1584 source_path])
1585 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1586 with open(test_tree_filepath, 'r') as f:
1587 content = f.read()
1588 files[relative_filepath] = content
1589 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001590 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001591
James E. Blaire18d4602017-01-05 11:17:28 -08001592 def assertNodepoolState(self):
1593 # Make sure that there are no pending requests
1594
1595 requests = self.fake_nodepool.getNodeRequests()
1596 self.assertEqual(len(requests), 0)
1597
1598 nodes = self.fake_nodepool.getNodes()
1599 for node in nodes:
1600 self.assertFalse(node['_lock'], "Node %s is locked" %
1601 (node['_oid'],))
1602
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001603 def assertNoGeneratedKeys(self):
1604 # Make sure that Zuul did not generate any project keys
1605 # (unless it was supposed to).
1606
1607 if self.create_project_keys:
1608 return
1609
1610 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1611 test_key = i.read()
1612
1613 key_root = os.path.join(self.state_root, 'keys')
1614 for root, dirname, files in os.walk(key_root):
1615 for fn in files:
1616 with open(os.path.join(root, fn)) as f:
1617 self.assertEqual(test_key, f.read())
1618
Clark Boylanb640e052014-04-03 16:41:46 -07001619 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001620 self.log.debug("Assert final state")
1621 # Make sure no jobs are running
1622 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001623 # Make sure that git.Repo objects have been garbage collected.
1624 repos = []
1625 gc.collect()
1626 for obj in gc.get_objects():
1627 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001628 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001629 repos.append(obj)
1630 self.assertEqual(len(repos), 0)
1631 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001632 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001633 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001634 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001635 for tenant in self.sched.abide.tenants.values():
1636 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001637 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001638 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001639
1640 def shutdown(self):
1641 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001642 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001643 self.merge_server.stop()
1644 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001645 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001646 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001647 self.sched.stop()
1648 self.sched.join()
1649 self.statsd.stop()
1650 self.statsd.join()
1651 self.webapp.stop()
1652 self.webapp.join()
1653 self.rpc.stop()
1654 self.rpc.join()
1655 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001656 self.fake_nodepool.stop()
1657 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001658 threads = threading.enumerate()
1659 if len(threads) > 1:
1660 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001661 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001662
James E. Blaira002b032017-04-18 10:35:48 -07001663 def assertCleanShutdown(self):
1664 pass
1665
Clark Boylanb640e052014-04-03 16:41:46 -07001666 def init_repo(self, project):
1667 parts = project.split('/')
1668 path = os.path.join(self.upstream_root, *parts[:-1])
1669 if not os.path.exists(path):
1670 os.makedirs(path)
1671 path = os.path.join(self.upstream_root, project)
1672 repo = git.Repo.init(path)
1673
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001674 with repo.config_writer() as config_writer:
1675 config_writer.set_value('user', 'email', 'user@example.com')
1676 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001677
Clark Boylanb640e052014-04-03 16:41:46 -07001678 repo.index.commit('initial commit')
1679 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001680
James E. Blair97d902e2014-08-21 13:25:56 -07001681 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001682 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001683 repo.git.clean('-x', '-f', '-d')
1684
James E. Blair97d902e2014-08-21 13:25:56 -07001685 def create_branch(self, project, branch):
1686 path = os.path.join(self.upstream_root, project)
1687 repo = git.Repo.init(path)
1688 fn = os.path.join(path, 'README')
1689
1690 branch_head = repo.create_head(branch)
1691 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001692 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001693 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001694 f.close()
1695 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001696 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001697
James E. Blair97d902e2014-08-21 13:25:56 -07001698 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001699 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001700 repo.git.clean('-x', '-f', '-d')
1701
Sachi King9f16d522016-03-16 12:20:45 +11001702 def create_commit(self, project):
1703 path = os.path.join(self.upstream_root, project)
1704 repo = git.Repo(path)
1705 repo.head.reference = repo.heads['master']
1706 file_name = os.path.join(path, 'README')
1707 with open(file_name, 'a') as f:
1708 f.write('creating fake commit\n')
1709 repo.index.add([file_name])
1710 commit = repo.index.commit('Creating a fake commit')
1711 return commit.hexsha
1712
James E. Blairf4a5f022017-04-18 14:01:10 -07001713 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001714 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001715 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001716 while len(self.builds):
1717 self.release(self.builds[0])
1718 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001719 i += 1
1720 if count is not None and i >= count:
1721 break
James E. Blairb8c16472015-05-05 14:55:26 -07001722
Clark Boylanb640e052014-04-03 16:41:46 -07001723 def release(self, job):
1724 if isinstance(job, FakeBuild):
1725 job.release()
1726 else:
1727 job.waiting = False
1728 self.log.debug("Queued job %s released" % job.unique)
1729 self.gearman_server.wakeConnections()
1730
1731 def getParameter(self, job, name):
1732 if isinstance(job, FakeBuild):
1733 return job.parameters[name]
1734 else:
1735 parameters = json.loads(job.arguments)
1736 return parameters[name]
1737
Clark Boylanb640e052014-04-03 16:41:46 -07001738 def haveAllBuildsReported(self):
1739 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001740 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001741 return False
1742 # Find out if every build that the worker has completed has been
1743 # reported back to Zuul. If it hasn't then that means a Gearman
1744 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001745 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001746 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001747 if not zbuild:
1748 # It has already been reported
1749 continue
1750 # It hasn't been reported yet.
1751 return False
1752 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001753 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001754 if connection.state == 'GRAB_WAIT':
1755 return False
1756 return True
1757
1758 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001759 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001760 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001761 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001762 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001763 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001764 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001765 for j in conn.related_jobs.values():
1766 if j.unique == build.uuid:
1767 client_job = j
1768 break
1769 if not client_job:
1770 self.log.debug("%s is not known to the gearman client" %
1771 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001772 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001773 if not client_job.handle:
1774 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001775 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001776 server_job = self.gearman_server.jobs.get(client_job.handle)
1777 if not server_job:
1778 self.log.debug("%s is not known to the gearman server" %
1779 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001780 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001781 if not hasattr(server_job, 'waiting'):
1782 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001783 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001784 if server_job.waiting:
1785 continue
James E. Blair17302972016-08-10 16:11:42 -07001786 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001787 self.log.debug("%s has not reported start" % build)
1788 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001789 worker_build = self.executor_server.job_builds.get(
1790 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001791 if worker_build:
1792 if worker_build.isWaiting():
1793 continue
1794 else:
1795 self.log.debug("%s is running" % worker_build)
1796 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001797 else:
James E. Blair962220f2016-08-03 11:22:38 -07001798 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001799 return False
James E. Blaira002b032017-04-18 10:35:48 -07001800 for (build_uuid, job_worker) in \
1801 self.executor_server.job_workers.items():
1802 if build_uuid not in seen_builds:
1803 self.log.debug("%s is not finalized" % build_uuid)
1804 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001805 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001806
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001808 if self.fake_nodepool.paused:
1809 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001810 if self.sched.nodepool.requests:
1811 return False
1812 return True
1813
Jan Hruban6b71aff2015-10-22 16:58:08 +02001814 def eventQueuesEmpty(self):
1815 for queue in self.event_queues:
1816 yield queue.empty()
1817
1818 def eventQueuesJoin(self):
1819 for queue in self.event_queues:
1820 queue.join()
1821
Clark Boylanb640e052014-04-03 16:41:46 -07001822 def waitUntilSettled(self):
1823 self.log.debug("Waiting until settled...")
1824 start = time.time()
1825 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001826 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001827 self.log.error("Timeout waiting for Zuul to settle")
1828 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001829 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001830 self.log.error(" %s: %s" % (queue, queue.empty()))
1831 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001832 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001833 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001834 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001835 self.log.error("All requests completed: %s" %
1836 (self.areAllNodeRequestsComplete(),))
1837 self.log.error("Merge client jobs: %s" %
1838 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001839 raise Exception("Timeout waiting for Zuul to settle")
1840 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001841
Paul Belanger174a8272017-03-14 13:20:10 -04001842 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001843 # have all build states propogated to zuul?
1844 if self.haveAllBuildsReported():
1845 # Join ensures that the queue is empty _and_ events have been
1846 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001847 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001848 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001849 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001850 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001851 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001852 self.areAllNodeRequestsComplete() and
1853 all(self.eventQueuesEmpty())):
1854 # The queue empty check is placed at the end to
1855 # ensure that if a component adds an event between
1856 # when locked the run handler and checked that the
1857 # components were stable, we don't erroneously
1858 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001859 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001860 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001861 self.log.debug("...settled.")
1862 return
1863 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001864 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001865 self.sched.wake_event.wait(0.1)
1866
1867 def countJobResults(self, jobs, result):
1868 jobs = filter(lambda x: x.result == result, jobs)
1869 return len(jobs)
1870
James E. Blair96c6bf82016-01-15 16:20:40 -08001871 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001872 for job in self.history:
1873 if (job.name == name and
1874 (project is None or
1875 job.parameters['ZUUL_PROJECT'] == project)):
1876 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001877 raise Exception("Unable to find job %s in history" % name)
1878
1879 def assertEmptyQueues(self):
1880 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001881 for tenant in self.sched.abide.tenants.values():
1882 for pipeline in tenant.layout.pipelines.values():
1883 for queue in pipeline.queues:
1884 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001885 print('pipeline %s queue %s contents %s' % (
1886 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001887 self.assertEqual(len(queue.queue), 0,
1888 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001889
1890 def assertReportedStat(self, key, value=None, kind=None):
1891 start = time.time()
1892 while time.time() < (start + 5):
1893 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001894 k, v = stat.split(':')
1895 if key == k:
1896 if value is None and kind is None:
1897 return
1898 elif value:
1899 if value == v:
1900 return
1901 elif kind:
1902 if v.endswith('|' + kind):
1903 return
1904 time.sleep(0.1)
1905
Clark Boylanb640e052014-04-03 16:41:46 -07001906 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001907
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001908 def assertBuilds(self, builds):
1909 """Assert that the running builds are as described.
1910
1911 The list of running builds is examined and must match exactly
1912 the list of builds described by the input.
1913
1914 :arg list builds: A list of dictionaries. Each item in the
1915 list must match the corresponding build in the build
1916 history, and each element of the dictionary must match the
1917 corresponding attribute of the build.
1918
1919 """
James E. Blair3158e282016-08-19 09:34:11 -07001920 try:
1921 self.assertEqual(len(self.builds), len(builds))
1922 for i, d in enumerate(builds):
1923 for k, v in d.items():
1924 self.assertEqual(
1925 getattr(self.builds[i], k), v,
1926 "Element %i in builds does not match" % (i,))
1927 except Exception:
1928 for build in self.builds:
1929 self.log.error("Running build: %s" % build)
1930 else:
1931 self.log.error("No running builds")
1932 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001933
James E. Blairb536ecc2016-08-31 10:11:42 -07001934 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001935 """Assert that the completed builds are as described.
1936
1937 The list of completed builds is examined and must match
1938 exactly the list of builds described by the input.
1939
1940 :arg list history: A list of dictionaries. Each item in the
1941 list must match the corresponding build in the build
1942 history, and each element of the dictionary must match the
1943 corresponding attribute of the build.
1944
James E. Blairb536ecc2016-08-31 10:11:42 -07001945 :arg bool ordered: If true, the history must match the order
1946 supplied, if false, the builds are permitted to have
1947 arrived in any order.
1948
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001949 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001950 def matches(history_item, item):
1951 for k, v in item.items():
1952 if getattr(history_item, k) != v:
1953 return False
1954 return True
James E. Blair3158e282016-08-19 09:34:11 -07001955 try:
1956 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001957 if ordered:
1958 for i, d in enumerate(history):
1959 if not matches(self.history[i], d):
1960 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001961 "Element %i in history does not match %s" %
1962 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001963 else:
1964 unseen = self.history[:]
1965 for i, d in enumerate(history):
1966 found = False
1967 for unseen_item in unseen:
1968 if matches(unseen_item, d):
1969 found = True
1970 unseen.remove(unseen_item)
1971 break
1972 if not found:
1973 raise Exception("No match found for element %i "
1974 "in history" % (i,))
1975 if unseen:
1976 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001977 except Exception:
1978 for build in self.history:
1979 self.log.error("Completed build: %s" % build)
1980 else:
1981 self.log.error("No completed builds")
1982 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001983
James E. Blair6ac368c2016-12-22 18:07:20 -08001984 def printHistory(self):
1985 """Log the build history.
1986
1987 This can be useful during tests to summarize what jobs have
1988 completed.
1989
1990 """
1991 self.log.debug("Build history:")
1992 for build in self.history:
1993 self.log.debug(build)
1994
James E. Blair59fdbac2015-12-07 17:08:06 -08001995 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08001996 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
1997
James E. Blair109da3f2017-04-04 14:39:43 -07001998 def updateConfigLayout(self, path, untrusted_projects=None):
1999 if untrusted_projects is None:
2000 untrusted_projects = []
James E. Blairf84026c2015-12-08 16:11:46 -08002001 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002002 if not os.path.exists(root):
2003 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002004 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2005 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002006- tenant:
2007 name: openstack
2008 source:
2009 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002010 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002011 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002012 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002013 - org/project
2014 - org/project1
2015 - org/project2
James E. Blair44860b62017-04-19 13:16:11 -07002016 - org/project3\n""" % path)
James E. Blair0ffa0102017-03-30 13:11:33 -07002017
James E. Blair109da3f2017-04-04 14:39:43 -07002018 for repo in untrusted_projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002019 f.write(" - %s\n" % repo)
James E. Blairf84026c2015-12-08 16:11:46 -08002020 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002021 self.config.set('zuul', 'tenant_config',
2022 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002023 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002024
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002025 def addCommitToRepo(self, project, message, files,
2026 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002027 path = os.path.join(self.upstream_root, project)
2028 repo = git.Repo(path)
2029 repo.head.reference = branch
2030 zuul.merger.merger.reset_repo_to_head(repo)
2031 for fn, content in files.items():
2032 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002033 try:
2034 os.makedirs(os.path.dirname(fn))
2035 except OSError:
2036 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002037 with open(fn, 'w') as f:
2038 f.write(content)
2039 repo.index.add([fn])
2040 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002041 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002042 repo.heads[branch].commit = commit
2043 repo.head.reference = branch
2044 repo.git.clean('-x', '-f', '-d')
2045 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002046 if tag:
2047 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002048 return before
2049
2050 def commitLayoutUpdate(self, orig_name, source_name):
2051 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002052 source_name)
2053 to_copy = ['zuul.yaml']
2054 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2055 to_copy.append('playbooks/{}'.format(playbook))
2056 commit_data = {}
2057 for source_file in to_copy:
2058 source_file_path = os.path.join(source_path, source_file)
2059 with open(source_file_path, 'r') as nt:
2060 commit_data[source_file] = nt.read()
2061 before = self.addCommitToRepo(
2062 orig_name, 'Pulling content from %s' % source_name,
2063 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002064 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002065
James E. Blair7fc8daa2016-08-08 15:37:15 -07002066 def addEvent(self, connection, event):
2067 """Inject a Fake (Gerrit) event.
2068
2069 This method accepts a JSON-encoded event and simulates Zuul
2070 having received it from Gerrit. It could (and should)
2071 eventually apply to any connection type, but is currently only
2072 used with Gerrit connections. The name of the connection is
2073 used to look up the corresponding server, and the event is
2074 simulated as having been received by all Zuul connections
2075 attached to that server. So if two Gerrit connections in Zuul
2076 are connected to the same Gerrit server, and you invoke this
2077 method specifying the name of one of them, the event will be
2078 received by both.
2079
2080 .. note::
2081
2082 "self.fake_gerrit.addEvent" calls should be migrated to
2083 this method.
2084
2085 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002086 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002087 :arg str event: The JSON-encoded event.
2088
2089 """
2090 specified_conn = self.connections.connections[connection]
2091 for conn in self.connections.connections.values():
2092 if (isinstance(conn, specified_conn.__class__) and
2093 specified_conn.server == conn.server):
2094 conn.addEvent(event)
2095
James E. Blair3f876d52016-07-22 13:07:14 -07002096
2097class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002098 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002099 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002100
Joshua Heskethd78b4482015-09-14 16:56:34 -06002101
2102class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002103 def setup_config(self):
2104 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002105 for section_name in self.config.sections():
2106 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2107 section_name, re.I)
2108 if not con_match:
2109 continue
2110
2111 if self.config.get(section_name, 'driver') == 'sql':
2112 f = MySQLSchemaFixture()
2113 self.useFixture(f)
2114 if (self.config.get(section_name, 'dburi') ==
2115 '$MYSQL_FIXTURE_DBURI$'):
2116 self.config.set(section_name, 'dburi', f.dburi)