blob: 2ad1085d8552aa2213d0d9c0601879cc47df5493 [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")
James E. Blair97d902e2014-08-21 13:25:56 -07001341 self.init_repo("org/project4")
James E. Blairbce35e12014-08-21 14:31:17 -07001342 self.init_repo("org/project5")
1343 self.init_repo("org/project6")
Clark Boylanb640e052014-04-03 16:41:46 -07001344 self.init_repo("org/one-job-project")
1345 self.init_repo("org/nonvoting-project")
1346 self.init_repo("org/templated-project")
1347 self.init_repo("org/layered-project")
1348 self.init_repo("org/node-project")
1349 self.init_repo("org/conflict-project")
1350 self.init_repo("org/noop-project")
Clark Boylanb640e052014-04-03 16:41:46 -07001351
1352 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001353 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1354 # see: https://github.com/jsocol/pystatsd/issues/61
1355 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001356 os.environ['STATSD_PORT'] = str(self.statsd.port)
1357 self.statsd.start()
1358 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001359 reload_module(statsd)
1360 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001361
1362 self.gearman_server = FakeGearmanServer()
1363
1364 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001365 self.log.info("Gearman server on port %s" %
1366 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001367
James E. Blaire511d2f2016-12-08 15:22:26 -08001368 gerritsource.GerritSource.replication_timeout = 1.5
1369 gerritsource.GerritSource.replication_retry_interval = 0.5
1370 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001371
Joshua Hesketh352264b2015-08-11 23:42:08 +10001372 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001373
Jan Hruban6b71aff2015-10-22 16:58:08 +02001374 self.event_queues = [
1375 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001376 self.sched.trigger_event_queue,
1377 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001378 ]
1379
James E. Blairfef78942016-03-11 16:28:56 -08001380 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001381 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001382
Clark Boylanb640e052014-04-03 16:41:46 -07001383 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001384 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001385 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001386 return FakeURLOpener(self.upstream_root, *args, **kw)
1387
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001388 old_urlopen = urllib.request.urlopen
1389 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001390
James E. Blair3f876d52016-07-22 13:07:14 -07001391 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001392
Paul Belanger174a8272017-03-14 13:20:10 -04001393 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001394 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001395 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001396 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001397 _test_root=self.test_root,
1398 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001399 self.executor_server.start()
1400 self.history = self.executor_server.build_history
1401 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001402
Paul Belanger174a8272017-03-14 13:20:10 -04001403 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001404 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001405 self.merge_client = zuul.merger.client.MergeClient(
1406 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001407 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001408 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001409 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001410
James E. Blair0d5a36e2017-02-21 10:53:44 -05001411 self.fake_nodepool = FakeNodepool(
1412 self.zk_chroot_fixture.zookeeper_host,
1413 self.zk_chroot_fixture.zookeeper_port,
1414 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001415
Paul Belanger174a8272017-03-14 13:20:10 -04001416 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001417 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001418 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001419 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001420
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001421 self.webapp = zuul.webapp.WebApp(
1422 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001423 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001424
1425 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001426 self.webapp.start()
1427 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001428 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001429 # Cleanups are run in reverse order
1430 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001431 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001432 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001433
James E. Blairb9c0d772017-03-03 14:34:49 -08001434 self.sched.reconfigure(self.config)
1435 self.sched.resume()
1436
James E. Blairfef78942016-03-11 16:28:56 -08001437 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001438 # Set up gerrit related fakes
1439 # Set a changes database so multiple FakeGerrit's can report back to
1440 # a virtual canonical database given by the configured hostname
1441 self.gerrit_changes_dbs = {}
1442
1443 def getGerritConnection(driver, name, config):
1444 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1445 con = FakeGerritConnection(driver, name, config,
1446 changes_db=db,
1447 upstream_root=self.upstream_root)
1448 self.event_queues.append(con.event_queue)
1449 setattr(self, 'fake_' + name, con)
1450 return con
1451
1452 self.useFixture(fixtures.MonkeyPatch(
1453 'zuul.driver.gerrit.GerritDriver.getConnection',
1454 getGerritConnection))
1455
1456 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001457 # TODO(jhesketh): This should come from lib.connections for better
1458 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001459 # Register connections from the config
1460 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001461
Joshua Hesketh352264b2015-08-11 23:42:08 +10001462 def FakeSMTPFactory(*args, **kw):
1463 args = [self.smtp_messages] + list(args)
1464 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001465
Joshua Hesketh352264b2015-08-11 23:42:08 +10001466 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001467
James E. Blaire511d2f2016-12-08 15:22:26 -08001468 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001469 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001470 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001471
James E. Blair83005782015-12-11 14:46:03 -08001472 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001473 # This creates the per-test configuration object. It can be
1474 # overriden by subclasses, but should not need to be since it
1475 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001476 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001477 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001478
1479 if not self.setupSimpleLayout():
1480 if hasattr(self, 'tenant_config_file'):
1481 self.config.set('zuul', 'tenant_config',
1482 self.tenant_config_file)
1483 git_path = os.path.join(
1484 os.path.dirname(
1485 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1486 'git')
1487 if os.path.exists(git_path):
1488 for reponame in os.listdir(git_path):
1489 project = reponame.replace('_', '/')
1490 self.copyDirToRepo(project,
1491 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001492 self.setupAllProjectKeys()
1493
James E. Blair06cc3922017-04-19 10:08:10 -07001494 def setupSimpleLayout(self):
1495 # If the test method has been decorated with a simple_layout,
1496 # use that instead of the class tenant_config_file. Set up a
1497 # single config-project with the specified layout, and
1498 # initialize repos for all of the 'project' entries which
1499 # appear in the layout.
1500 test_name = self.id().split('.')[-1]
1501 test = getattr(self, test_name)
1502 if hasattr(test, '__simple_layout__'):
1503 path = getattr(test, '__simple_layout__')
1504 else:
1505 return False
1506
James E. Blairb70e55a2017-04-19 12:57:02 -07001507 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001508 path = os.path.join(FIXTURE_DIR, path)
1509 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001510 data = f.read()
1511 layout = yaml.safe_load(data)
1512 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001513 untrusted_projects = []
1514 for item in layout:
1515 if 'project' in item:
1516 name = item['project']['name']
1517 untrusted_projects.append(name)
1518 self.init_repo(name)
1519 self.addCommitToRepo(name, 'initial commit',
1520 files={'README': ''},
1521 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001522 if 'job' in item:
1523 jobname = item['job']['name']
1524 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001525
1526 root = os.path.join(self.test_root, "config")
1527 if not os.path.exists(root):
1528 os.makedirs(root)
1529 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1530 config = [{'tenant':
1531 {'name': 'tenant-one',
1532 'source': {'gerrit':
1533 {'config-projects': ['common-config'],
1534 'untrusted-projects': untrusted_projects}}}}]
1535 f.write(yaml.dump(config))
1536 f.close()
1537 self.config.set('zuul', 'tenant_config',
1538 os.path.join(FIXTURE_DIR, f.name))
1539
1540 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001541 self.addCommitToRepo('common-config', 'add content from fixture',
1542 files, branch='master', tag='init')
1543
1544 return True
1545
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001546 def setupAllProjectKeys(self):
1547 if self.create_project_keys:
1548 return
1549
1550 path = self.config.get('zuul', 'tenant_config')
1551 with open(os.path.join(FIXTURE_DIR, path)) as f:
1552 tenant_config = yaml.safe_load(f.read())
1553 for tenant in tenant_config:
1554 sources = tenant['tenant']['source']
1555 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001556 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001557 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001558 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001559 self.setupProjectKeys(source, project)
1560
1561 def setupProjectKeys(self, source, project):
1562 # Make sure we set up an RSA key for the project so that we
1563 # don't spend time generating one:
1564
1565 key_root = os.path.join(self.state_root, 'keys')
1566 if not os.path.isdir(key_root):
1567 os.mkdir(key_root, 0o700)
1568 private_key_file = os.path.join(key_root, source, project + '.pem')
1569 private_key_dir = os.path.dirname(private_key_file)
1570 self.log.debug("Installing test keys for project %s at %s" % (
1571 project, private_key_file))
1572 if not os.path.isdir(private_key_dir):
1573 os.makedirs(private_key_dir)
1574 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1575 with open(private_key_file, 'w') as o:
1576 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001577
James E. Blair498059b2016-12-20 13:50:13 -08001578 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001579 self.zk_chroot_fixture = self.useFixture(
1580 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001581 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001582 self.zk_chroot_fixture.zookeeper_host,
1583 self.zk_chroot_fixture.zookeeper_port,
1584 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001585
James E. Blair96c6bf82016-01-15 16:20:40 -08001586 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001587 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001588
1589 files = {}
1590 for (dirpath, dirnames, filenames) in os.walk(source_path):
1591 for filename in filenames:
1592 test_tree_filepath = os.path.join(dirpath, filename)
1593 common_path = os.path.commonprefix([test_tree_filepath,
1594 source_path])
1595 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1596 with open(test_tree_filepath, 'r') as f:
1597 content = f.read()
1598 files[relative_filepath] = content
1599 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001600 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001601
James E. Blaire18d4602017-01-05 11:17:28 -08001602 def assertNodepoolState(self):
1603 # Make sure that there are no pending requests
1604
1605 requests = self.fake_nodepool.getNodeRequests()
1606 self.assertEqual(len(requests), 0)
1607
1608 nodes = self.fake_nodepool.getNodes()
1609 for node in nodes:
1610 self.assertFalse(node['_lock'], "Node %s is locked" %
1611 (node['_oid'],))
1612
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001613 def assertNoGeneratedKeys(self):
1614 # Make sure that Zuul did not generate any project keys
1615 # (unless it was supposed to).
1616
1617 if self.create_project_keys:
1618 return
1619
1620 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1621 test_key = i.read()
1622
1623 key_root = os.path.join(self.state_root, 'keys')
1624 for root, dirname, files in os.walk(key_root):
1625 for fn in files:
1626 with open(os.path.join(root, fn)) as f:
1627 self.assertEqual(test_key, f.read())
1628
Clark Boylanb640e052014-04-03 16:41:46 -07001629 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001630 self.log.debug("Assert final state")
1631 # Make sure no jobs are running
1632 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001633 # Make sure that git.Repo objects have been garbage collected.
1634 repos = []
1635 gc.collect()
1636 for obj in gc.get_objects():
1637 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001638 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001639 repos.append(obj)
1640 self.assertEqual(len(repos), 0)
1641 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001642 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001643 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001644 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001645 for tenant in self.sched.abide.tenants.values():
1646 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001647 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001648 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001649
1650 def shutdown(self):
1651 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001652 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001653 self.merge_server.stop()
1654 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001655 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001656 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001657 self.sched.stop()
1658 self.sched.join()
1659 self.statsd.stop()
1660 self.statsd.join()
1661 self.webapp.stop()
1662 self.webapp.join()
1663 self.rpc.stop()
1664 self.rpc.join()
1665 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001666 self.fake_nodepool.stop()
1667 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001668 threads = threading.enumerate()
1669 if len(threads) > 1:
1670 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001671 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001672
James E. Blaira002b032017-04-18 10:35:48 -07001673 def assertCleanShutdown(self):
1674 pass
1675
Clark Boylanb640e052014-04-03 16:41:46 -07001676 def init_repo(self, project):
1677 parts = project.split('/')
1678 path = os.path.join(self.upstream_root, *parts[:-1])
1679 if not os.path.exists(path):
1680 os.makedirs(path)
1681 path = os.path.join(self.upstream_root, project)
1682 repo = git.Repo.init(path)
1683
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001684 with repo.config_writer() as config_writer:
1685 config_writer.set_value('user', 'email', 'user@example.com')
1686 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001687
Clark Boylanb640e052014-04-03 16:41:46 -07001688 repo.index.commit('initial commit')
1689 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001690
James E. Blair97d902e2014-08-21 13:25:56 -07001691 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001692 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001693 repo.git.clean('-x', '-f', '-d')
1694
James E. Blair97d902e2014-08-21 13:25:56 -07001695 def create_branch(self, project, branch):
1696 path = os.path.join(self.upstream_root, project)
1697 repo = git.Repo.init(path)
1698 fn = os.path.join(path, 'README')
1699
1700 branch_head = repo.create_head(branch)
1701 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001702 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001703 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001704 f.close()
1705 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001706 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001707
James E. Blair97d902e2014-08-21 13:25:56 -07001708 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001709 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001710 repo.git.clean('-x', '-f', '-d')
1711
Sachi King9f16d522016-03-16 12:20:45 +11001712 def create_commit(self, project):
1713 path = os.path.join(self.upstream_root, project)
1714 repo = git.Repo(path)
1715 repo.head.reference = repo.heads['master']
1716 file_name = os.path.join(path, 'README')
1717 with open(file_name, 'a') as f:
1718 f.write('creating fake commit\n')
1719 repo.index.add([file_name])
1720 commit = repo.index.commit('Creating a fake commit')
1721 return commit.hexsha
1722
James E. Blairf4a5f022017-04-18 14:01:10 -07001723 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001724 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001725 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001726 while len(self.builds):
1727 self.release(self.builds[0])
1728 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001729 i += 1
1730 if count is not None and i >= count:
1731 break
James E. Blairb8c16472015-05-05 14:55:26 -07001732
Clark Boylanb640e052014-04-03 16:41:46 -07001733 def release(self, job):
1734 if isinstance(job, FakeBuild):
1735 job.release()
1736 else:
1737 job.waiting = False
1738 self.log.debug("Queued job %s released" % job.unique)
1739 self.gearman_server.wakeConnections()
1740
1741 def getParameter(self, job, name):
1742 if isinstance(job, FakeBuild):
1743 return job.parameters[name]
1744 else:
1745 parameters = json.loads(job.arguments)
1746 return parameters[name]
1747
Clark Boylanb640e052014-04-03 16:41:46 -07001748 def haveAllBuildsReported(self):
1749 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001750 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001751 return False
1752 # Find out if every build that the worker has completed has been
1753 # reported back to Zuul. If it hasn't then that means a Gearman
1754 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001755 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001756 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001757 if not zbuild:
1758 # It has already been reported
1759 continue
1760 # It hasn't been reported yet.
1761 return False
1762 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001763 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001764 if connection.state == 'GRAB_WAIT':
1765 return False
1766 return True
1767
1768 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001769 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001770 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001771 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001772 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001773 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001774 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001775 for j in conn.related_jobs.values():
1776 if j.unique == build.uuid:
1777 client_job = j
1778 break
1779 if not client_job:
1780 self.log.debug("%s is not known to the gearman client" %
1781 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001782 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001783 if not client_job.handle:
1784 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001785 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001786 server_job = self.gearman_server.jobs.get(client_job.handle)
1787 if not server_job:
1788 self.log.debug("%s is not known to the gearman server" %
1789 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001790 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001791 if not hasattr(server_job, 'waiting'):
1792 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001793 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001794 if server_job.waiting:
1795 continue
James E. Blair17302972016-08-10 16:11:42 -07001796 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001797 self.log.debug("%s has not reported start" % build)
1798 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001799 worker_build = self.executor_server.job_builds.get(
1800 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001801 if worker_build:
1802 if worker_build.isWaiting():
1803 continue
1804 else:
1805 self.log.debug("%s is running" % worker_build)
1806 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001807 else:
James E. Blair962220f2016-08-03 11:22:38 -07001808 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001809 return False
James E. Blaira002b032017-04-18 10:35:48 -07001810 for (build_uuid, job_worker) in \
1811 self.executor_server.job_workers.items():
1812 if build_uuid not in seen_builds:
1813 self.log.debug("%s is not finalized" % build_uuid)
1814 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001815 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001816
James E. Blairdce6cea2016-12-20 16:45:32 -08001817 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001818 if self.fake_nodepool.paused:
1819 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001820 if self.sched.nodepool.requests:
1821 return False
1822 return True
1823
Jan Hruban6b71aff2015-10-22 16:58:08 +02001824 def eventQueuesEmpty(self):
1825 for queue in self.event_queues:
1826 yield queue.empty()
1827
1828 def eventQueuesJoin(self):
1829 for queue in self.event_queues:
1830 queue.join()
1831
Clark Boylanb640e052014-04-03 16:41:46 -07001832 def waitUntilSettled(self):
1833 self.log.debug("Waiting until settled...")
1834 start = time.time()
1835 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001836 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001837 self.log.error("Timeout waiting for Zuul to settle")
1838 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001839 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001840 self.log.error(" %s: %s" % (queue, queue.empty()))
1841 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001842 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001843 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001844 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001845 self.log.error("All requests completed: %s" %
1846 (self.areAllNodeRequestsComplete(),))
1847 self.log.error("Merge client jobs: %s" %
1848 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001849 raise Exception("Timeout waiting for Zuul to settle")
1850 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001851
Paul Belanger174a8272017-03-14 13:20:10 -04001852 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001853 # have all build states propogated to zuul?
1854 if self.haveAllBuildsReported():
1855 # Join ensures that the queue is empty _and_ events have been
1856 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001857 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001858 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001859 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001860 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001861 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001862 self.areAllNodeRequestsComplete() and
1863 all(self.eventQueuesEmpty())):
1864 # The queue empty check is placed at the end to
1865 # ensure that if a component adds an event between
1866 # when locked the run handler and checked that the
1867 # components were stable, we don't erroneously
1868 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001869 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001870 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001871 self.log.debug("...settled.")
1872 return
1873 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001874 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001875 self.sched.wake_event.wait(0.1)
1876
1877 def countJobResults(self, jobs, result):
1878 jobs = filter(lambda x: x.result == result, jobs)
1879 return len(jobs)
1880
James E. Blair96c6bf82016-01-15 16:20:40 -08001881 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001882 for job in self.history:
1883 if (job.name == name and
1884 (project is None or
1885 job.parameters['ZUUL_PROJECT'] == project)):
1886 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001887 raise Exception("Unable to find job %s in history" % name)
1888
1889 def assertEmptyQueues(self):
1890 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001891 for tenant in self.sched.abide.tenants.values():
1892 for pipeline in tenant.layout.pipelines.values():
1893 for queue in pipeline.queues:
1894 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001895 print('pipeline %s queue %s contents %s' % (
1896 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001897 self.assertEqual(len(queue.queue), 0,
1898 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001899
1900 def assertReportedStat(self, key, value=None, kind=None):
1901 start = time.time()
1902 while time.time() < (start + 5):
1903 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001904 k, v = stat.split(':')
1905 if key == k:
1906 if value is None and kind is None:
1907 return
1908 elif value:
1909 if value == v:
1910 return
1911 elif kind:
1912 if v.endswith('|' + kind):
1913 return
1914 time.sleep(0.1)
1915
Clark Boylanb640e052014-04-03 16:41:46 -07001916 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001917
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001918 def assertBuilds(self, builds):
1919 """Assert that the running builds are as described.
1920
1921 The list of running builds is examined and must match exactly
1922 the list of builds described by the input.
1923
1924 :arg list builds: A list of dictionaries. Each item in the
1925 list must match the corresponding build in the build
1926 history, and each element of the dictionary must match the
1927 corresponding attribute of the build.
1928
1929 """
James E. Blair3158e282016-08-19 09:34:11 -07001930 try:
1931 self.assertEqual(len(self.builds), len(builds))
1932 for i, d in enumerate(builds):
1933 for k, v in d.items():
1934 self.assertEqual(
1935 getattr(self.builds[i], k), v,
1936 "Element %i in builds does not match" % (i,))
1937 except Exception:
1938 for build in self.builds:
1939 self.log.error("Running build: %s" % build)
1940 else:
1941 self.log.error("No running builds")
1942 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001943
James E. Blairb536ecc2016-08-31 10:11:42 -07001944 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001945 """Assert that the completed builds are as described.
1946
1947 The list of completed builds is examined and must match
1948 exactly the list of builds described by the input.
1949
1950 :arg list history: A list of dictionaries. Each item in the
1951 list must match the corresponding build in the build
1952 history, and each element of the dictionary must match the
1953 corresponding attribute of the build.
1954
James E. Blairb536ecc2016-08-31 10:11:42 -07001955 :arg bool ordered: If true, the history must match the order
1956 supplied, if false, the builds are permitted to have
1957 arrived in any order.
1958
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001959 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001960 def matches(history_item, item):
1961 for k, v in item.items():
1962 if getattr(history_item, k) != v:
1963 return False
1964 return True
James E. Blair3158e282016-08-19 09:34:11 -07001965 try:
1966 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001967 if ordered:
1968 for i, d in enumerate(history):
1969 if not matches(self.history[i], d):
1970 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001971 "Element %i in history does not match %s" %
1972 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001973 else:
1974 unseen = self.history[:]
1975 for i, d in enumerate(history):
1976 found = False
1977 for unseen_item in unseen:
1978 if matches(unseen_item, d):
1979 found = True
1980 unseen.remove(unseen_item)
1981 break
1982 if not found:
1983 raise Exception("No match found for element %i "
1984 "in history" % (i,))
1985 if unseen:
1986 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001987 except Exception:
1988 for build in self.history:
1989 self.log.error("Completed build: %s" % build)
1990 else:
1991 self.log.error("No completed builds")
1992 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001993
James E. Blair6ac368c2016-12-22 18:07:20 -08001994 def printHistory(self):
1995 """Log the build history.
1996
1997 This can be useful during tests to summarize what jobs have
1998 completed.
1999
2000 """
2001 self.log.debug("Build history:")
2002 for build in self.history:
2003 self.log.debug(build)
2004
James E. Blair59fdbac2015-12-07 17:08:06 -08002005 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002006 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2007
James E. Blair109da3f2017-04-04 14:39:43 -07002008 def updateConfigLayout(self, path, untrusted_projects=None):
2009 if untrusted_projects is None:
2010 untrusted_projects = []
James E. Blairf84026c2015-12-08 16:11:46 -08002011 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002012 if not os.path.exists(root):
2013 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002014 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2015 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002016- tenant:
2017 name: openstack
2018 source:
2019 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002020 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002021 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002022 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002023 - org/project
2024 - org/project1
2025 - org/project2
2026 - org/project3
2027 - org/project4
2028 - org/project5
2029 - org/project6
2030 - org/one-job-project
2031 - org/nonvoting-project
2032 - org/templated-project
2033 - org/layered-project
2034 - org/node-project
2035 - org/conflict-project
James E. Blairb70e55a2017-04-19 12:57:02 -07002036 - org/noop-project\n""" % path)
James E. Blair0ffa0102017-03-30 13:11:33 -07002037
James E. Blair109da3f2017-04-04 14:39:43 -07002038 for repo in untrusted_projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002039 f.write(" - %s\n" % repo)
James E. Blairf84026c2015-12-08 16:11:46 -08002040 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002041 self.config.set('zuul', 'tenant_config',
2042 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002043 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002044
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002045 def addCommitToRepo(self, project, message, files,
2046 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002047 path = os.path.join(self.upstream_root, project)
2048 repo = git.Repo(path)
2049 repo.head.reference = branch
2050 zuul.merger.merger.reset_repo_to_head(repo)
2051 for fn, content in files.items():
2052 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002053 try:
2054 os.makedirs(os.path.dirname(fn))
2055 except OSError:
2056 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002057 with open(fn, 'w') as f:
2058 f.write(content)
2059 repo.index.add([fn])
2060 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002061 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002062 repo.heads[branch].commit = commit
2063 repo.head.reference = branch
2064 repo.git.clean('-x', '-f', '-d')
2065 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002066 if tag:
2067 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002068 return before
2069
2070 def commitLayoutUpdate(self, orig_name, source_name):
2071 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002072 source_name)
2073 to_copy = ['zuul.yaml']
2074 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2075 to_copy.append('playbooks/{}'.format(playbook))
2076 commit_data = {}
2077 for source_file in to_copy:
2078 source_file_path = os.path.join(source_path, source_file)
2079 with open(source_file_path, 'r') as nt:
2080 commit_data[source_file] = nt.read()
2081 before = self.addCommitToRepo(
2082 orig_name, 'Pulling content from %s' % source_name,
2083 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002084 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002085
James E. Blair7fc8daa2016-08-08 15:37:15 -07002086 def addEvent(self, connection, event):
2087 """Inject a Fake (Gerrit) event.
2088
2089 This method accepts a JSON-encoded event and simulates Zuul
2090 having received it from Gerrit. It could (and should)
2091 eventually apply to any connection type, but is currently only
2092 used with Gerrit connections. The name of the connection is
2093 used to look up the corresponding server, and the event is
2094 simulated as having been received by all Zuul connections
2095 attached to that server. So if two Gerrit connections in Zuul
2096 are connected to the same Gerrit server, and you invoke this
2097 method specifying the name of one of them, the event will be
2098 received by both.
2099
2100 .. note::
2101
2102 "self.fake_gerrit.addEvent" calls should be migrated to
2103 this method.
2104
2105 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002106 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002107 :arg str event: The JSON-encoded event.
2108
2109 """
2110 specified_conn = self.connections.connections[connection]
2111 for conn in self.connections.connections.values():
2112 if (isinstance(conn, specified_conn.__class__) and
2113 specified_conn.server == conn.server):
2114 conn.addEvent(event)
2115
James E. Blair3f876d52016-07-22 13:07:14 -07002116
2117class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002118 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002119 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002120
Joshua Heskethd78b4482015-09-14 16:56:34 -06002121
2122class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002123 def setup_config(self):
2124 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002125 for section_name in self.config.sections():
2126 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2127 section_name, re.I)
2128 if not con_match:
2129 continue
2130
2131 if self.config.get(section_name, 'driver') == 'sql':
2132 f = MySQLSchemaFixture()
2133 self.useFixture(f)
2134 if (self.config.get(section_name, 'dburi') ==
2135 '$MYSQL_FIXTURE_DBURI$'):
2136 self.config.set(section_name, 'dburi', f.dburi)