blob: 120920bf82cb15b94537aea0ec1db1ae33303644 [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")
1351 self.init_repo("org/experimental-project")
1352
1353 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001354 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1355 # see: https://github.com/jsocol/pystatsd/issues/61
1356 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001357 os.environ['STATSD_PORT'] = str(self.statsd.port)
1358 self.statsd.start()
1359 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001360 reload_module(statsd)
1361 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001362
1363 self.gearman_server = FakeGearmanServer()
1364
1365 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001366 self.log.info("Gearman server on port %s" %
1367 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001368
James E. Blaire511d2f2016-12-08 15:22:26 -08001369 gerritsource.GerritSource.replication_timeout = 1.5
1370 gerritsource.GerritSource.replication_retry_interval = 0.5
1371 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001372
Joshua Hesketh352264b2015-08-11 23:42:08 +10001373 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001374
Jan Hruban6b71aff2015-10-22 16:58:08 +02001375 self.event_queues = [
1376 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001377 self.sched.trigger_event_queue,
1378 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001379 ]
1380
James E. Blairfef78942016-03-11 16:28:56 -08001381 self.configure_connections()
Joshua Hesketh352264b2015-08-11 23:42:08 +10001382 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001383
Clark Boylanb640e052014-04-03 16:41:46 -07001384 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001385 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001386 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001387 return FakeURLOpener(self.upstream_root, *args, **kw)
1388
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001389 old_urlopen = urllib.request.urlopen
1390 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001391
James E. Blair3f876d52016-07-22 13:07:14 -07001392 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001393
Paul Belanger174a8272017-03-14 13:20:10 -04001394 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001395 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001396 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001397 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001398 _test_root=self.test_root,
1399 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001400 self.executor_server.start()
1401 self.history = self.executor_server.build_history
1402 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001403
Paul Belanger174a8272017-03-14 13:20:10 -04001404 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001405 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001406 self.merge_client = zuul.merger.client.MergeClient(
1407 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001408 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001409 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001410 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001411
James E. Blair0d5a36e2017-02-21 10:53:44 -05001412 self.fake_nodepool = FakeNodepool(
1413 self.zk_chroot_fixture.zookeeper_host,
1414 self.zk_chroot_fixture.zookeeper_port,
1415 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001416
Paul Belanger174a8272017-03-14 13:20:10 -04001417 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001418 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001419 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001420 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001421
Paul Belanger88ef0ea2015-12-23 11:57:02 -05001422 self.webapp = zuul.webapp.WebApp(
1423 self.sched, port=0, listen_address='127.0.0.1')
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001424 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001425
1426 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001427 self.webapp.start()
1428 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001429 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001430 # Cleanups are run in reverse order
1431 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001432 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001433 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001434
James E. Blairb9c0d772017-03-03 14:34:49 -08001435 self.sched.reconfigure(self.config)
1436 self.sched.resume()
1437
James E. Blairfef78942016-03-11 16:28:56 -08001438 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001439 # Set up gerrit related fakes
1440 # Set a changes database so multiple FakeGerrit's can report back to
1441 # a virtual canonical database given by the configured hostname
1442 self.gerrit_changes_dbs = {}
1443
1444 def getGerritConnection(driver, name, config):
1445 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1446 con = FakeGerritConnection(driver, name, config,
1447 changes_db=db,
1448 upstream_root=self.upstream_root)
1449 self.event_queues.append(con.event_queue)
1450 setattr(self, 'fake_' + name, con)
1451 return con
1452
1453 self.useFixture(fixtures.MonkeyPatch(
1454 'zuul.driver.gerrit.GerritDriver.getConnection',
1455 getGerritConnection))
1456
1457 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001458 # TODO(jhesketh): This should come from lib.connections for better
1459 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001460 # Register connections from the config
1461 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001462
Joshua Hesketh352264b2015-08-11 23:42:08 +10001463 def FakeSMTPFactory(*args, **kw):
1464 args = [self.smtp_messages] + list(args)
1465 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001466
Joshua Hesketh352264b2015-08-11 23:42:08 +10001467 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001468
James E. Blaire511d2f2016-12-08 15:22:26 -08001469 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001470 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001471 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001472
James E. Blair83005782015-12-11 14:46:03 -08001473 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001474 # This creates the per-test configuration object. It can be
1475 # overriden by subclasses, but should not need to be since it
1476 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001477 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001478 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001479
1480 if not self.setupSimpleLayout():
1481 if hasattr(self, 'tenant_config_file'):
1482 self.config.set('zuul', 'tenant_config',
1483 self.tenant_config_file)
1484 git_path = os.path.join(
1485 os.path.dirname(
1486 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1487 'git')
1488 if os.path.exists(git_path):
1489 for reponame in os.listdir(git_path):
1490 project = reponame.replace('_', '/')
1491 self.copyDirToRepo(project,
1492 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001493 self.setupAllProjectKeys()
1494
James E. Blair06cc3922017-04-19 10:08:10 -07001495 def setupSimpleLayout(self):
1496 # If the test method has been decorated with a simple_layout,
1497 # use that instead of the class tenant_config_file. Set up a
1498 # single config-project with the specified layout, and
1499 # initialize repos for all of the 'project' entries which
1500 # appear in the layout.
1501 test_name = self.id().split('.')[-1]
1502 test = getattr(self, test_name)
1503 if hasattr(test, '__simple_layout__'):
1504 path = getattr(test, '__simple_layout__')
1505 else:
1506 return False
1507
1508 path = os.path.join(FIXTURE_DIR, path)
1509 with open(path) as f:
1510 layout = yaml.safe_load(f.read())
1511 untrusted_projects = []
1512 for item in layout:
1513 if 'project' in item:
1514 name = item['project']['name']
1515 untrusted_projects.append(name)
1516 self.init_repo(name)
1517 self.addCommitToRepo(name, 'initial commit',
1518 files={'README': ''},
1519 branch='master', tag='init')
1520
1521 root = os.path.join(self.test_root, "config")
1522 if not os.path.exists(root):
1523 os.makedirs(root)
1524 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1525 config = [{'tenant':
1526 {'name': 'tenant-one',
1527 'source': {'gerrit':
1528 {'config-projects': ['common-config'],
1529 'untrusted-projects': untrusted_projects}}}}]
1530 f.write(yaml.dump(config))
1531 f.close()
1532 self.config.set('zuul', 'tenant_config',
1533 os.path.join(FIXTURE_DIR, f.name))
1534
1535 self.init_repo('common-config')
1536 with open(path) as f:
1537 files = {'zuul.yaml': f.read()}
1538 self.addCommitToRepo('common-config', 'add content from fixture',
1539 files, branch='master', tag='init')
1540
1541 return True
1542
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001543 def setupAllProjectKeys(self):
1544 if self.create_project_keys:
1545 return
1546
1547 path = self.config.get('zuul', 'tenant_config')
1548 with open(os.path.join(FIXTURE_DIR, path)) as f:
1549 tenant_config = yaml.safe_load(f.read())
1550 for tenant in tenant_config:
1551 sources = tenant['tenant']['source']
1552 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001553 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001554 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001555 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001556 self.setupProjectKeys(source, project)
1557
1558 def setupProjectKeys(self, source, project):
1559 # Make sure we set up an RSA key for the project so that we
1560 # don't spend time generating one:
1561
1562 key_root = os.path.join(self.state_root, 'keys')
1563 if not os.path.isdir(key_root):
1564 os.mkdir(key_root, 0o700)
1565 private_key_file = os.path.join(key_root, source, project + '.pem')
1566 private_key_dir = os.path.dirname(private_key_file)
1567 self.log.debug("Installing test keys for project %s at %s" % (
1568 project, private_key_file))
1569 if not os.path.isdir(private_key_dir):
1570 os.makedirs(private_key_dir)
1571 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1572 with open(private_key_file, 'w') as o:
1573 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001574
James E. Blair498059b2016-12-20 13:50:13 -08001575 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001576 self.zk_chroot_fixture = self.useFixture(
1577 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001578 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001579 self.zk_chroot_fixture.zookeeper_host,
1580 self.zk_chroot_fixture.zookeeper_port,
1581 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001582
James E. Blair96c6bf82016-01-15 16:20:40 -08001583 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001584 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001585
1586 files = {}
1587 for (dirpath, dirnames, filenames) in os.walk(source_path):
1588 for filename in filenames:
1589 test_tree_filepath = os.path.join(dirpath, filename)
1590 common_path = os.path.commonprefix([test_tree_filepath,
1591 source_path])
1592 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1593 with open(test_tree_filepath, 'r') as f:
1594 content = f.read()
1595 files[relative_filepath] = content
1596 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001597 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001598
James E. Blaire18d4602017-01-05 11:17:28 -08001599 def assertNodepoolState(self):
1600 # Make sure that there are no pending requests
1601
1602 requests = self.fake_nodepool.getNodeRequests()
1603 self.assertEqual(len(requests), 0)
1604
1605 nodes = self.fake_nodepool.getNodes()
1606 for node in nodes:
1607 self.assertFalse(node['_lock'], "Node %s is locked" %
1608 (node['_oid'],))
1609
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001610 def assertNoGeneratedKeys(self):
1611 # Make sure that Zuul did not generate any project keys
1612 # (unless it was supposed to).
1613
1614 if self.create_project_keys:
1615 return
1616
1617 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1618 test_key = i.read()
1619
1620 key_root = os.path.join(self.state_root, 'keys')
1621 for root, dirname, files in os.walk(key_root):
1622 for fn in files:
1623 with open(os.path.join(root, fn)) as f:
1624 self.assertEqual(test_key, f.read())
1625
Clark Boylanb640e052014-04-03 16:41:46 -07001626 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001627 self.log.debug("Assert final state")
1628 # Make sure no jobs are running
1629 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001630 # Make sure that git.Repo objects have been garbage collected.
1631 repos = []
1632 gc.collect()
1633 for obj in gc.get_objects():
1634 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001635 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001636 repos.append(obj)
1637 self.assertEqual(len(repos), 0)
1638 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001639 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001640 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001641 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001642 for tenant in self.sched.abide.tenants.values():
1643 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001644 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001645 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001646
1647 def shutdown(self):
1648 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001649 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001650 self.merge_server.stop()
1651 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001652 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001653 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001654 self.sched.stop()
1655 self.sched.join()
1656 self.statsd.stop()
1657 self.statsd.join()
1658 self.webapp.stop()
1659 self.webapp.join()
1660 self.rpc.stop()
1661 self.rpc.join()
1662 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001663 self.fake_nodepool.stop()
1664 self.zk.disconnect()
Clark Boylanb640e052014-04-03 16:41:46 -07001665 threads = threading.enumerate()
1666 if len(threads) > 1:
1667 self.log.error("More than one thread is running: %s" % threads)
James E. Blair6ac368c2016-12-22 18:07:20 -08001668 self.printHistory()
Clark Boylanb640e052014-04-03 16:41:46 -07001669
James E. Blaira002b032017-04-18 10:35:48 -07001670 def assertCleanShutdown(self):
1671 pass
1672
Clark Boylanb640e052014-04-03 16:41:46 -07001673 def init_repo(self, project):
1674 parts = project.split('/')
1675 path = os.path.join(self.upstream_root, *parts[:-1])
1676 if not os.path.exists(path):
1677 os.makedirs(path)
1678 path = os.path.join(self.upstream_root, project)
1679 repo = git.Repo.init(path)
1680
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001681 with repo.config_writer() as config_writer:
1682 config_writer.set_value('user', 'email', 'user@example.com')
1683 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001684
Clark Boylanb640e052014-04-03 16:41:46 -07001685 repo.index.commit('initial commit')
1686 master = repo.create_head('master')
Clark Boylanb640e052014-04-03 16:41:46 -07001687
James E. Blair97d902e2014-08-21 13:25:56 -07001688 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001689 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001690 repo.git.clean('-x', '-f', '-d')
1691
James E. Blair97d902e2014-08-21 13:25:56 -07001692 def create_branch(self, project, branch):
1693 path = os.path.join(self.upstream_root, project)
1694 repo = git.Repo.init(path)
1695 fn = os.path.join(path, 'README')
1696
1697 branch_head = repo.create_head(branch)
1698 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001699 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001700 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001701 f.close()
1702 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001703 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001704
James E. Blair97d902e2014-08-21 13:25:56 -07001705 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001706 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001707 repo.git.clean('-x', '-f', '-d')
1708
Sachi King9f16d522016-03-16 12:20:45 +11001709 def create_commit(self, project):
1710 path = os.path.join(self.upstream_root, project)
1711 repo = git.Repo(path)
1712 repo.head.reference = repo.heads['master']
1713 file_name = os.path.join(path, 'README')
1714 with open(file_name, 'a') as f:
1715 f.write('creating fake commit\n')
1716 repo.index.add([file_name])
1717 commit = repo.index.commit('Creating a fake commit')
1718 return commit.hexsha
1719
James E. Blairf4a5f022017-04-18 14:01:10 -07001720 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001721 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001722 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001723 while len(self.builds):
1724 self.release(self.builds[0])
1725 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001726 i += 1
1727 if count is not None and i >= count:
1728 break
James E. Blairb8c16472015-05-05 14:55:26 -07001729
Clark Boylanb640e052014-04-03 16:41:46 -07001730 def release(self, job):
1731 if isinstance(job, FakeBuild):
1732 job.release()
1733 else:
1734 job.waiting = False
1735 self.log.debug("Queued job %s released" % job.unique)
1736 self.gearman_server.wakeConnections()
1737
1738 def getParameter(self, job, name):
1739 if isinstance(job, FakeBuild):
1740 return job.parameters[name]
1741 else:
1742 parameters = json.loads(job.arguments)
1743 return parameters[name]
1744
Clark Boylanb640e052014-04-03 16:41:46 -07001745 def haveAllBuildsReported(self):
1746 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001747 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001748 return False
1749 # Find out if every build that the worker has completed has been
1750 # reported back to Zuul. If it hasn't then that means a Gearman
1751 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001752 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001753 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001754 if not zbuild:
1755 # It has already been reported
1756 continue
1757 # It hasn't been reported yet.
1758 return False
1759 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001760 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001761 if connection.state == 'GRAB_WAIT':
1762 return False
1763 return True
1764
1765 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001766 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001767 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001768 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001769 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001770 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001771 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001772 for j in conn.related_jobs.values():
1773 if j.unique == build.uuid:
1774 client_job = j
1775 break
1776 if not client_job:
1777 self.log.debug("%s is not known to the gearman client" %
1778 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001779 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001780 if not client_job.handle:
1781 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001782 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001783 server_job = self.gearman_server.jobs.get(client_job.handle)
1784 if not server_job:
1785 self.log.debug("%s is not known to the gearman server" %
1786 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001787 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001788 if not hasattr(server_job, 'waiting'):
1789 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001790 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001791 if server_job.waiting:
1792 continue
James E. Blair17302972016-08-10 16:11:42 -07001793 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001794 self.log.debug("%s has not reported start" % build)
1795 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001796 worker_build = self.executor_server.job_builds.get(
1797 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001798 if worker_build:
1799 if worker_build.isWaiting():
1800 continue
1801 else:
1802 self.log.debug("%s is running" % worker_build)
1803 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001804 else:
James E. Blair962220f2016-08-03 11:22:38 -07001805 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001806 return False
James E. Blaira002b032017-04-18 10:35:48 -07001807 for (build_uuid, job_worker) in \
1808 self.executor_server.job_workers.items():
1809 if build_uuid not in seen_builds:
1810 self.log.debug("%s is not finalized" % build_uuid)
1811 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001812 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001813
James E. Blairdce6cea2016-12-20 16:45:32 -08001814 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001815 if self.fake_nodepool.paused:
1816 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001817 if self.sched.nodepool.requests:
1818 return False
1819 return True
1820
Jan Hruban6b71aff2015-10-22 16:58:08 +02001821 def eventQueuesEmpty(self):
1822 for queue in self.event_queues:
1823 yield queue.empty()
1824
1825 def eventQueuesJoin(self):
1826 for queue in self.event_queues:
1827 queue.join()
1828
Clark Boylanb640e052014-04-03 16:41:46 -07001829 def waitUntilSettled(self):
1830 self.log.debug("Waiting until settled...")
1831 start = time.time()
1832 while True:
Clint Byruma9626572017-02-22 14:04:00 -05001833 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001834 self.log.error("Timeout waiting for Zuul to settle")
1835 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07001836 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08001837 self.log.error(" %s: %s" % (queue, queue.empty()))
1838 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07001839 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001840 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07001841 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08001842 self.log.error("All requests completed: %s" %
1843 (self.areAllNodeRequestsComplete(),))
1844 self.log.error("Merge client jobs: %s" %
1845 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07001846 raise Exception("Timeout waiting for Zuul to settle")
1847 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07001848
Paul Belanger174a8272017-03-14 13:20:10 -04001849 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07001850 # have all build states propogated to zuul?
1851 if self.haveAllBuildsReported():
1852 # Join ensures that the queue is empty _and_ events have been
1853 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02001854 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07001855 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08001856 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07001857 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08001858 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08001859 self.areAllNodeRequestsComplete() and
1860 all(self.eventQueuesEmpty())):
1861 # The queue empty check is placed at the end to
1862 # ensure that if a component adds an event between
1863 # when locked the run handler and checked that the
1864 # components were stable, we don't erroneously
1865 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07001866 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001867 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001868 self.log.debug("...settled.")
1869 return
1870 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001871 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07001872 self.sched.wake_event.wait(0.1)
1873
1874 def countJobResults(self, jobs, result):
1875 jobs = filter(lambda x: x.result == result, jobs)
1876 return len(jobs)
1877
James E. Blair96c6bf82016-01-15 16:20:40 -08001878 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07001879 for job in self.history:
1880 if (job.name == name and
1881 (project is None or
1882 job.parameters['ZUUL_PROJECT'] == project)):
1883 return job
Clark Boylanb640e052014-04-03 16:41:46 -07001884 raise Exception("Unable to find job %s in history" % name)
1885
1886 def assertEmptyQueues(self):
1887 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08001888 for tenant in self.sched.abide.tenants.values():
1889 for pipeline in tenant.layout.pipelines.values():
1890 for queue in pipeline.queues:
1891 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10001892 print('pipeline %s queue %s contents %s' % (
1893 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08001894 self.assertEqual(len(queue.queue), 0,
1895 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07001896
1897 def assertReportedStat(self, key, value=None, kind=None):
1898 start = time.time()
1899 while time.time() < (start + 5):
1900 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07001901 k, v = stat.split(':')
1902 if key == k:
1903 if value is None and kind is None:
1904 return
1905 elif value:
1906 if value == v:
1907 return
1908 elif kind:
1909 if v.endswith('|' + kind):
1910 return
1911 time.sleep(0.1)
1912
Clark Boylanb640e052014-04-03 16:41:46 -07001913 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08001914
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001915 def assertBuilds(self, builds):
1916 """Assert that the running builds are as described.
1917
1918 The list of running builds is examined and must match exactly
1919 the list of builds described by the input.
1920
1921 :arg list builds: A list of dictionaries. Each item in the
1922 list must match the corresponding build in the build
1923 history, and each element of the dictionary must match the
1924 corresponding attribute of the build.
1925
1926 """
James E. Blair3158e282016-08-19 09:34:11 -07001927 try:
1928 self.assertEqual(len(self.builds), len(builds))
1929 for i, d in enumerate(builds):
1930 for k, v in d.items():
1931 self.assertEqual(
1932 getattr(self.builds[i], k), v,
1933 "Element %i in builds does not match" % (i,))
1934 except Exception:
1935 for build in self.builds:
1936 self.log.error("Running build: %s" % build)
1937 else:
1938 self.log.error("No running builds")
1939 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001940
James E. Blairb536ecc2016-08-31 10:11:42 -07001941 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001942 """Assert that the completed builds are as described.
1943
1944 The list of completed builds is examined and must match
1945 exactly the list of builds described by the input.
1946
1947 :arg list history: A list of dictionaries. Each item in the
1948 list must match the corresponding build in the build
1949 history, and each element of the dictionary must match the
1950 corresponding attribute of the build.
1951
James E. Blairb536ecc2016-08-31 10:11:42 -07001952 :arg bool ordered: If true, the history must match the order
1953 supplied, if false, the builds are permitted to have
1954 arrived in any order.
1955
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001956 """
James E. Blairb536ecc2016-08-31 10:11:42 -07001957 def matches(history_item, item):
1958 for k, v in item.items():
1959 if getattr(history_item, k) != v:
1960 return False
1961 return True
James E. Blair3158e282016-08-19 09:34:11 -07001962 try:
1963 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07001964 if ordered:
1965 for i, d in enumerate(history):
1966 if not matches(self.history[i], d):
1967 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001968 "Element %i in history does not match %s" %
1969 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07001970 else:
1971 unseen = self.history[:]
1972 for i, d in enumerate(history):
1973 found = False
1974 for unseen_item in unseen:
1975 if matches(unseen_item, d):
1976 found = True
1977 unseen.remove(unseen_item)
1978 break
1979 if not found:
1980 raise Exception("No match found for element %i "
1981 "in history" % (i,))
1982 if unseen:
1983 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07001984 except Exception:
1985 for build in self.history:
1986 self.log.error("Completed build: %s" % build)
1987 else:
1988 self.log.error("No completed builds")
1989 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07001990
James E. Blair6ac368c2016-12-22 18:07:20 -08001991 def printHistory(self):
1992 """Log the build history.
1993
1994 This can be useful during tests to summarize what jobs have
1995 completed.
1996
1997 """
1998 self.log.debug("Build history:")
1999 for build in self.history:
2000 self.log.debug(build)
2001
James E. Blair59fdbac2015-12-07 17:08:06 -08002002 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002003 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2004
James E. Blair109da3f2017-04-04 14:39:43 -07002005 def updateConfigLayout(self, path, untrusted_projects=None):
2006 if untrusted_projects is None:
2007 untrusted_projects = []
James E. Blairf84026c2015-12-08 16:11:46 -08002008 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002009 if not os.path.exists(root):
2010 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002011 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2012 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002013- tenant:
2014 name: openstack
2015 source:
2016 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002017 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002018 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002019 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002020 - org/project
2021 - org/project1
2022 - org/project2
2023 - org/project3
2024 - org/project4
2025 - org/project5
2026 - org/project6
2027 - org/one-job-project
2028 - org/nonvoting-project
2029 - org/templated-project
2030 - org/layered-project
2031 - org/node-project
2032 - org/conflict-project
2033 - org/noop-project
James E. Blair06cc3922017-04-19 10:08:10 -07002034 - org/experimental-project\n""" % path)
James E. Blair0ffa0102017-03-30 13:11:33 -07002035
James E. Blair109da3f2017-04-04 14:39:43 -07002036 for repo in untrusted_projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002037 f.write(" - %s\n" % repo)
James E. Blairf84026c2015-12-08 16:11:46 -08002038 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002039 self.config.set('zuul', 'tenant_config',
2040 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002041 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002042
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002043 def addCommitToRepo(self, project, message, files,
2044 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002045 path = os.path.join(self.upstream_root, project)
2046 repo = git.Repo(path)
2047 repo.head.reference = branch
2048 zuul.merger.merger.reset_repo_to_head(repo)
2049 for fn, content in files.items():
2050 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002051 try:
2052 os.makedirs(os.path.dirname(fn))
2053 except OSError:
2054 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002055 with open(fn, 'w') as f:
2056 f.write(content)
2057 repo.index.add([fn])
2058 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002059 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002060 repo.heads[branch].commit = commit
2061 repo.head.reference = branch
2062 repo.git.clean('-x', '-f', '-d')
2063 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002064 if tag:
2065 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002066 return before
2067
2068 def commitLayoutUpdate(self, orig_name, source_name):
2069 source_path = os.path.join(self.test_root, 'upstream',
Clint Byrum678e2c32017-03-16 16:27:21 -07002070 source_name)
2071 to_copy = ['zuul.yaml']
2072 for playbook in os.listdir(os.path.join(source_path, 'playbooks')):
2073 to_copy.append('playbooks/{}'.format(playbook))
2074 commit_data = {}
2075 for source_file in to_copy:
2076 source_file_path = os.path.join(source_path, source_file)
2077 with open(source_file_path, 'r') as nt:
2078 commit_data[source_file] = nt.read()
2079 before = self.addCommitToRepo(
2080 orig_name, 'Pulling content from %s' % source_name,
2081 commit_data)
Clint Byrum58264dc2017-02-07 21:21:22 -08002082 return before
James E. Blair3f876d52016-07-22 13:07:14 -07002083
James E. Blair7fc8daa2016-08-08 15:37:15 -07002084 def addEvent(self, connection, event):
2085 """Inject a Fake (Gerrit) event.
2086
2087 This method accepts a JSON-encoded event and simulates Zuul
2088 having received it from Gerrit. It could (and should)
2089 eventually apply to any connection type, but is currently only
2090 used with Gerrit connections. The name of the connection is
2091 used to look up the corresponding server, and the event is
2092 simulated as having been received by all Zuul connections
2093 attached to that server. So if two Gerrit connections in Zuul
2094 are connected to the same Gerrit server, and you invoke this
2095 method specifying the name of one of them, the event will be
2096 received by both.
2097
2098 .. note::
2099
2100 "self.fake_gerrit.addEvent" calls should be migrated to
2101 this method.
2102
2103 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002104 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002105 :arg str event: The JSON-encoded event.
2106
2107 """
2108 specified_conn = self.connections.connections[connection]
2109 for conn in self.connections.connections.values():
2110 if (isinstance(conn, specified_conn.__class__) and
2111 specified_conn.server == conn.server):
2112 conn.addEvent(event)
2113
James E. Blair3f876d52016-07-22 13:07:14 -07002114
2115class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002116 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002117 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002118
Joshua Heskethd78b4482015-09-14 16:56:34 -06002119
2120class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002121 def setup_config(self):
2122 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002123 for section_name in self.config.sections():
2124 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2125 section_name, re.I)
2126 if not con_match:
2127 continue
2128
2129 if self.config.get(section_name, 'driver') == 'sql':
2130 f = MySQLSchemaFixture()
2131 self.useFixture(f)
2132 if (self.config.get(section_name, 'dburi') ==
2133 '$MYSQL_FIXTURE_DBURI$'):
2134 self.config.set(section_name, 'dburi', f.dburi)