blob: e33b5102f37c00a05ea42b812fb9395111f077f6 [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
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
92 return hashlib.sha1(str(random.random())).hexdigest()
93
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
James E. Blair7fc8daa2016-08-08 15:37:15 -0700463 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
James E. Blair8b5408c2016-08-08 15:37:46 -0700498 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499 if 'label' in action:
500 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000501 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000504
Clark Boylanb640e052014-04-03 16:41:46 -0700505 if 'submit' in action:
506 change.setMerged()
507 if message:
508 change.setReported()
509
510 def query(self, number):
511 change = self.changes.get(int(number))
512 if change:
513 return change.query()
514 return {}
515
James E. Blairc494d542014-08-06 09:23:52 -0700516 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700517 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700518 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800519 if query.startswith('change:'):
520 # Query a specific changeid
521 changeid = query[len('change:'):]
522 l = [change.query() for change in self.changes.values()
523 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700524 elif query.startswith('message:'):
525 # Query the content of a commit message
526 msg = query[len('message:'):].strip()
527 l = [change.query() for change in self.changes.values()
528 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800529 else:
530 # Query all open changes
531 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700532 return l
James E. Blairc494d542014-08-06 09:23:52 -0700533
Joshua Hesketh352264b2015-08-11 23:42:08 +1000534 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700535 pass
536
Joshua Hesketh352264b2015-08-11 23:42:08 +1000537 def getGitUrl(self, project):
538 return os.path.join(self.upstream_root, project.name)
539
Clark Boylanb640e052014-04-03 16:41:46 -0700540
Gregory Haynes4fc12542015-04-22 20:38:06 -0700541class GithubChangeReference(git.Reference):
542 _common_path_default = "refs/pull"
543 _points_to_commits_only = True
544
545
546class FakeGithubPullRequest(object):
547
548 def __init__(self, github, number, project, branch,
549 upstream_root, number_of_commits=1):
550 """Creates a new PR with several commits.
551 Sends an event about opened PR."""
552 self.github = github
553 self.source = github
554 self.number = number
555 self.project = project
556 self.branch = branch
557 self.upstream_root = upstream_root
558 self.comments = []
559 self.updated_at = None
560 self.head_sha = None
561 self._createPRRef()
562 self._addCommitToRepo()
563 self._updateTimeStamp()
564
565 def addCommit(self):
566 """Adds a commit on top of the actual PR head."""
567 self._addCommitToRepo()
568 self._updateTimeStamp()
569
570 def forcePush(self):
571 """Clears actual commits and add a commit on top of the base."""
572 self._addCommitToRepo(reset=True)
573 self._updateTimeStamp()
574
575 def getPullRequestOpenedEvent(self):
576 return self._getPullRequestEvent('opened')
577
578 def getPullRequestSynchronizeEvent(self):
579 return self._getPullRequestEvent('synchronize')
580
581 def getPullRequestReopenedEvent(self):
582 return self._getPullRequestEvent('reopened')
583
584 def getPullRequestClosedEvent(self):
585 return self._getPullRequestEvent('closed')
586
587 def addComment(self, message):
588 self.comments.append(message)
589 self._updateTimeStamp()
590
591 def _getRepo(self):
592 repo_path = os.path.join(self.upstream_root, self.project)
593 return git.Repo(repo_path)
594
595 def _createPRRef(self):
596 repo = self._getRepo()
597 GithubChangeReference.create(
598 repo, self._getPRReference(), 'refs/tags/init')
599
600 def _addCommitToRepo(self, reset=False):
601 repo = self._getRepo()
602 ref = repo.references[self._getPRReference()]
603 if reset:
604 ref.set_object('refs/tags/init')
605 repo.head.reference = ref
606 zuul.merger.merger.reset_repo_to_head(repo)
607 repo.git.clean('-x', '-f', '-d')
608
609 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
610 msg = 'test-%s' % self.number
611 fn = os.path.join(repo.working_dir, fn)
612 f = open(fn, 'w')
613 with open(fn, 'w') as f:
614 f.write("test %s %s\n" %
615 (self.branch, self.number))
616 repo.index.add([fn])
617
618 self.head_sha = repo.index.commit(msg).hexsha
619 repo.head.reference = 'master'
620 zuul.merger.merger.reset_repo_to_head(repo)
621 repo.git.clean('-x', '-f', '-d')
622 repo.heads['master'].checkout()
623
624 def _updateTimeStamp(self):
625 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
626
627 def getPRHeadSha(self):
628 repo = self._getRepo()
629 return repo.references[self._getPRReference()].commit.hexsha
630
631 def _getPRReference(self):
632 return '%s/head' % self.number
633
634 def _getPullRequestEvent(self, action):
635 name = 'pull_request'
636 data = {
637 'action': action,
638 'number': self.number,
639 'pull_request': {
640 'number': self.number,
641 'updated_at': self.updated_at,
642 'base': {
643 'ref': self.branch,
644 'repo': {
645 'full_name': self.project
646 }
647 },
648 'head': {
649 'sha': self.head_sha
650 }
651 }
652 }
653 return (name, data)
654
655
656class FakeGithubConnection(githubconnection.GithubConnection):
657 log = logging.getLogger("zuul.test.FakeGithubConnection")
658
659 def __init__(self, driver, connection_name, connection_config,
660 upstream_root=None):
661 super(FakeGithubConnection, self).__init__(driver, connection_name,
662 connection_config)
663 self.connection_name = connection_name
664 self.pr_number = 0
665 self.pull_requests = []
666 self.upstream_root = upstream_root
667
668 def openFakePullRequest(self, project, branch):
669 self.pr_number += 1
670 pull_request = FakeGithubPullRequest(
671 self, self.pr_number, project, branch, self.upstream_root)
672 self.pull_requests.append(pull_request)
673 return pull_request
674
675 def emitEvent(self, event):
676 """Emulates sending the GitHub webhook event to the connection."""
677 port = self.webapp.server.socket.getsockname()[1]
678 name, data = event
679 payload = json.dumps(data)
680 headers = {'X-Github-Event': name}
681 req = urllib.request.Request(
682 'http://localhost:%s/connection/%s/payload'
683 % (port, self.connection_name),
684 data=payload, headers=headers)
685 urllib.request.urlopen(req)
686
687 def getGitUrl(self, project):
688 return os.path.join(self.upstream_root, str(project))
689
690 def getProjectBranches(self, project):
691 """Masks getProjectBranches since we don't have a real github"""
692
693 # just returns master for now
694 return ['master']
695
696
Clark Boylanb640e052014-04-03 16:41:46 -0700697class BuildHistory(object):
698 def __init__(self, **kw):
699 self.__dict__.update(kw)
700
701 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700702 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
703 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700704
705
706class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200707 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700708 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700709 self.url = url
710
711 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700712 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700713 path = res.path
714 project = '/'.join(path.split('/')[2:-2])
715 ret = '001e# service=git-upload-pack\n'
716 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
717 'multi_ack thin-pack side-band side-band-64k ofs-delta '
718 'shallow no-progress include-tag multi_ack_detailed no-done\n')
719 path = os.path.join(self.upstream_root, project)
720 repo = git.Repo(path)
721 for ref in repo.refs:
722 r = ref.object.hexsha + ' ' + ref.path + '\n'
723 ret += '%04x%s' % (len(r) + 4, r)
724 ret += '0000'
725 return ret
726
727
Clark Boylanb640e052014-04-03 16:41:46 -0700728class FakeStatsd(threading.Thread):
729 def __init__(self):
730 threading.Thread.__init__(self)
731 self.daemon = True
732 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
733 self.sock.bind(('', 0))
734 self.port = self.sock.getsockname()[1]
735 self.wake_read, self.wake_write = os.pipe()
736 self.stats = []
737
738 def run(self):
739 while True:
740 poll = select.poll()
741 poll.register(self.sock, select.POLLIN)
742 poll.register(self.wake_read, select.POLLIN)
743 ret = poll.poll()
744 for (fd, event) in ret:
745 if fd == self.sock.fileno():
746 data = self.sock.recvfrom(1024)
747 if not data:
748 return
749 self.stats.append(data[0])
750 if fd == self.wake_read:
751 return
752
753 def stop(self):
754 os.write(self.wake_write, '1\n')
755
756
James E. Blaire1767bc2016-08-02 10:00:27 -0700757class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700758 log = logging.getLogger("zuul.test")
759
Paul Belanger174a8272017-03-14 13:20:10 -0400760 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700761 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400762 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700763 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700764 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700765 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700766 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700767 # TODOv3(jeblair): self.node is really "the image of the node
768 # assigned". We should rename it (self.node_image?) if we
769 # keep using it like this, or we may end up exposing more of
770 # the complexity around multi-node jobs here
771 # (self.nodes[0].image?)
772 self.node = None
773 if len(self.parameters.get('nodes')) == 1:
774 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700775 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100776 self.pipeline = self.parameters['ZUUL_PIPELINE']
777 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700778 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700779 self.wait_condition = threading.Condition()
780 self.waiting = False
781 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500782 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700783 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700784 self.changes = None
785 if 'ZUUL_CHANGE_IDS' in self.parameters:
786 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700787
James E. Blair3158e282016-08-19 09:34:11 -0700788 def __repr__(self):
789 waiting = ''
790 if self.waiting:
791 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100792 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
793 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700794
Clark Boylanb640e052014-04-03 16:41:46 -0700795 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700796 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700797 self.wait_condition.acquire()
798 self.wait_condition.notify()
799 self.waiting = False
800 self.log.debug("Build %s released" % self.unique)
801 self.wait_condition.release()
802
803 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700804 """Return whether this build is being held.
805
806 :returns: Whether the build is being held.
807 :rtype: bool
808 """
809
Clark Boylanb640e052014-04-03 16:41:46 -0700810 self.wait_condition.acquire()
811 if self.waiting:
812 ret = True
813 else:
814 ret = False
815 self.wait_condition.release()
816 return ret
817
818 def _wait(self):
819 self.wait_condition.acquire()
820 self.waiting = True
821 self.log.debug("Build %s waiting" % self.unique)
822 self.wait_condition.wait()
823 self.wait_condition.release()
824
825 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700826 self.log.debug('Running build %s' % self.unique)
827
Paul Belanger174a8272017-03-14 13:20:10 -0400828 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700829 self.log.debug('Holding build %s' % self.unique)
830 self._wait()
831 self.log.debug("Build %s continuing" % self.unique)
832
James E. Blair412fba82017-01-26 15:00:50 -0800833 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700834 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800835 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700836 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800837 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500838 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800839 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700840
James E. Blaire1767bc2016-08-02 10:00:27 -0700841 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700842
James E. Blaira5dba232016-08-08 15:53:24 -0700843 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400844 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700845 for change in changes:
846 if self.hasChanges(change):
847 return True
848 return False
849
James E. Blaire7b99a02016-08-05 14:27:34 -0700850 def hasChanges(self, *changes):
851 """Return whether this build has certain changes in its git repos.
852
853 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700854 are expected to be present (in order) in the git repository of
855 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700856
857 :returns: Whether the build has the indicated changes.
858 :rtype: bool
859
860 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800861 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -0700862 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -0700863 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800864 try:
865 repo = git.Repo(path)
866 except NoSuchPathError as e:
867 self.log.debug('%s' % e)
868 return False
869 ref = self.parameters['ZUUL_REF']
870 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
871 commit_message = '%s-1' % change.subject
872 self.log.debug("Checking if build %s has changes; commit_message "
873 "%s; repo_messages %s" % (self, commit_message,
874 repo_messages))
875 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700876 self.log.debug(" messages do not match")
877 return False
878 self.log.debug(" OK")
879 return True
880
Clark Boylanb640e052014-04-03 16:41:46 -0700881
Paul Belanger174a8272017-03-14 13:20:10 -0400882class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
883 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700884
Paul Belanger174a8272017-03-14 13:20:10 -0400885 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700886 they will report that they have started but then pause until
887 released before reporting completion. This attribute may be
888 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400889 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700890 be explicitly released.
891
892 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800893 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700894 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800895 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400896 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700897 self.hold_jobs_in_build = False
898 self.lock = threading.Lock()
899 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700900 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700901 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700902 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800903
James E. Blaira5dba232016-08-08 15:53:24 -0700904 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400905 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700906
907 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700908 :arg Change change: The :py:class:`~tests.base.FakeChange`
909 instance which should cause the job to fail. This job
910 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700911
912 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700913 l = self.fail_tests.get(name, [])
914 l.append(change)
915 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800916
James E. Blair962220f2016-08-03 11:22:38 -0700917 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700918 """Release a held build.
919
920 :arg str regex: A regular expression which, if supplied, will
921 cause only builds with matching names to be released. If
922 not supplied, all builds will be released.
923
924 """
James E. Blair962220f2016-08-03 11:22:38 -0700925 builds = self.running_builds[:]
926 self.log.debug("Releasing build %s (%s)" % (regex,
927 len(self.running_builds)))
928 for build in builds:
929 if not regex or re.match(regex, build.name):
930 self.log.debug("Releasing build %s" %
931 (build.parameters['ZUUL_UUID']))
932 build.release()
933 else:
934 self.log.debug("Not releasing build %s" %
935 (build.parameters['ZUUL_UUID']))
936 self.log.debug("Done releasing builds %s (%s)" %
937 (regex, len(self.running_builds)))
938
Paul Belanger174a8272017-03-14 13:20:10 -0400939 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700940 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700941 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700942 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700943 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800944 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500945 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800946 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100947 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
948 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700949
950 def stopJob(self, job):
951 self.log.debug("handle stop")
952 parameters = json.loads(job.arguments)
953 uuid = parameters['uuid']
954 for build in self.running_builds:
955 if build.unique == uuid:
956 build.aborted = True
957 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400958 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700959
James E. Blaira002b032017-04-18 10:35:48 -0700960 def stop(self):
961 for build in self.running_builds:
962 build.release()
963 super(RecordingExecutorServer, self).stop()
964
Joshua Hesketh50c21782016-10-13 21:34:14 +1100965
Paul Belanger174a8272017-03-14 13:20:10 -0400966class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700967 def doMergeChanges(self, items):
968 # Get a merger in order to update the repos involved in this job.
969 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
970 if not commit: # merge conflict
971 self.recordResult('MERGER_FAILURE')
972 return commit
973
974 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400975 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400976 self.executor_server.lock.acquire()
977 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700978 BuildHistory(name=build.name, result=result, changes=build.changes,
979 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800980 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700981 pipeline=build.parameters['ZUUL_PIPELINE'])
982 )
Paul Belanger174a8272017-03-14 13:20:10 -0400983 self.executor_server.running_builds.remove(build)
984 del self.executor_server.job_builds[self.job.unique]
985 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700986
987 def runPlaybooks(self, args):
988 build = self.executor_server.job_builds[self.job.unique]
989 build.jobdir = self.jobdir
990
991 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
992 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800993 return result
994
Monty Taylore6562aa2017-02-20 07:37:39 -0500995 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -0400996 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -0800997
Paul Belanger174a8272017-03-14 13:20:10 -0400998 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -0600999 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001000 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001001 else:
1002 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001003 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001004
James E. Blairad8dca02017-02-21 11:48:32 -05001005 def getHostList(self, args):
1006 self.log.debug("hostlist")
1007 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001008 for host in hosts:
1009 host['host_vars']['ansible_connection'] = 'local'
1010
1011 hosts.append(dict(
1012 name='localhost',
1013 host_vars=dict(ansible_connection='local'),
1014 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001015 return hosts
1016
James E. Blairf5dbd002015-12-23 15:26:17 -08001017
Clark Boylanb640e052014-04-03 16:41:46 -07001018class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001019 """A Gearman server for use in tests.
1020
1021 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1022 added to the queue but will not be distributed to workers
1023 until released. This attribute may be changed at any time and
1024 will take effect for subsequently enqueued jobs, but
1025 previously held jobs will still need to be explicitly
1026 released.
1027
1028 """
1029
Clark Boylanb640e052014-04-03 16:41:46 -07001030 def __init__(self):
1031 self.hold_jobs_in_queue = False
1032 super(FakeGearmanServer, self).__init__(0)
1033
1034 def getJobForConnection(self, connection, peek=False):
1035 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1036 for job in queue:
1037 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001038 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001039 job.waiting = self.hold_jobs_in_queue
1040 else:
1041 job.waiting = False
1042 if job.waiting:
1043 continue
1044 if job.name in connection.functions:
1045 if not peek:
1046 queue.remove(job)
1047 connection.related_jobs[job.handle] = job
1048 job.worker_connection = connection
1049 job.running = True
1050 return job
1051 return None
1052
1053 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001054 """Release a held job.
1055
1056 :arg str regex: A regular expression which, if supplied, will
1057 cause only jobs with matching names to be released. If
1058 not supplied, all jobs will be released.
1059 """
Clark Boylanb640e052014-04-03 16:41:46 -07001060 released = False
1061 qlen = (len(self.high_queue) + len(self.normal_queue) +
1062 len(self.low_queue))
1063 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1064 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001065 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001066 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001067 parameters = json.loads(job.arguments)
1068 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001069 self.log.debug("releasing queued job %s" %
1070 job.unique)
1071 job.waiting = False
1072 released = True
1073 else:
1074 self.log.debug("not releasing queued job %s" %
1075 job.unique)
1076 if released:
1077 self.wakeConnections()
1078 qlen = (len(self.high_queue) + len(self.normal_queue) +
1079 len(self.low_queue))
1080 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1081
1082
1083class FakeSMTP(object):
1084 log = logging.getLogger('zuul.FakeSMTP')
1085
1086 def __init__(self, messages, server, port):
1087 self.server = server
1088 self.port = port
1089 self.messages = messages
1090
1091 def sendmail(self, from_email, to_email, msg):
1092 self.log.info("Sending email from %s, to %s, with msg %s" % (
1093 from_email, to_email, msg))
1094
1095 headers = msg.split('\n\n', 1)[0]
1096 body = msg.split('\n\n', 1)[1]
1097
1098 self.messages.append(dict(
1099 from_email=from_email,
1100 to_email=to_email,
1101 msg=msg,
1102 headers=headers,
1103 body=body,
1104 ))
1105
1106 return True
1107
1108 def quit(self):
1109 return True
1110
1111
James E. Blairdce6cea2016-12-20 16:45:32 -08001112class FakeNodepool(object):
1113 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001114 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001115
1116 log = logging.getLogger("zuul.test.FakeNodepool")
1117
1118 def __init__(self, host, port, chroot):
1119 self.client = kazoo.client.KazooClient(
1120 hosts='%s:%s%s' % (host, port, chroot))
1121 self.client.start()
1122 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001123 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001124 self.thread = threading.Thread(target=self.run)
1125 self.thread.daemon = True
1126 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001127 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001128
1129 def stop(self):
1130 self._running = False
1131 self.thread.join()
1132 self.client.stop()
1133 self.client.close()
1134
1135 def run(self):
1136 while self._running:
1137 self._run()
1138 time.sleep(0.1)
1139
1140 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001141 if self.paused:
1142 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001143 for req in self.getNodeRequests():
1144 self.fulfillRequest(req)
1145
1146 def getNodeRequests(self):
1147 try:
1148 reqids = self.client.get_children(self.REQUEST_ROOT)
1149 except kazoo.exceptions.NoNodeError:
1150 return []
1151 reqs = []
1152 for oid in sorted(reqids):
1153 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001154 try:
1155 data, stat = self.client.get(path)
1156 data = json.loads(data)
1157 data['_oid'] = oid
1158 reqs.append(data)
1159 except kazoo.exceptions.NoNodeError:
1160 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001161 return reqs
1162
James E. Blaire18d4602017-01-05 11:17:28 -08001163 def getNodes(self):
1164 try:
1165 nodeids = self.client.get_children(self.NODE_ROOT)
1166 except kazoo.exceptions.NoNodeError:
1167 return []
1168 nodes = []
1169 for oid in sorted(nodeids):
1170 path = self.NODE_ROOT + '/' + oid
1171 data, stat = self.client.get(path)
1172 data = json.loads(data)
1173 data['_oid'] = oid
1174 try:
1175 lockfiles = self.client.get_children(path + '/lock')
1176 except kazoo.exceptions.NoNodeError:
1177 lockfiles = []
1178 if lockfiles:
1179 data['_lock'] = True
1180 else:
1181 data['_lock'] = False
1182 nodes.append(data)
1183 return nodes
1184
James E. Blaira38c28e2017-01-04 10:33:20 -08001185 def makeNode(self, request_id, node_type):
1186 now = time.time()
1187 path = '/nodepool/nodes/'
1188 data = dict(type=node_type,
1189 provider='test-provider',
1190 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001191 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001192 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001193 public_ipv4='127.0.0.1',
1194 private_ipv4=None,
1195 public_ipv6=None,
1196 allocated_to=request_id,
1197 state='ready',
1198 state_time=now,
1199 created_time=now,
1200 updated_time=now,
1201 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001202 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001203 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001204 data = json.dumps(data)
1205 path = self.client.create(path, data,
1206 makepath=True,
1207 sequence=True)
1208 nodeid = path.split("/")[-1]
1209 return nodeid
1210
James E. Blair6ab79e02017-01-06 10:10:17 -08001211 def addFailRequest(self, request):
1212 self.fail_requests.add(request['_oid'])
1213
James E. Blairdce6cea2016-12-20 16:45:32 -08001214 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001215 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001216 return
1217 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001218 oid = request['_oid']
1219 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001220
James E. Blair6ab79e02017-01-06 10:10:17 -08001221 if oid in self.fail_requests:
1222 request['state'] = 'failed'
1223 else:
1224 request['state'] = 'fulfilled'
1225 nodes = []
1226 for node in request['node_types']:
1227 nodeid = self.makeNode(oid, node)
1228 nodes.append(nodeid)
1229 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001230
James E. Blaira38c28e2017-01-04 10:33:20 -08001231 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001232 path = self.REQUEST_ROOT + '/' + oid
1233 data = json.dumps(request)
1234 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1235 self.client.set(path, data)
1236
1237
James E. Blair498059b2016-12-20 13:50:13 -08001238class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001239 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001240 super(ChrootedKazooFixture, self).__init__()
1241
1242 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1243 if ':' in zk_host:
1244 host, port = zk_host.split(':')
1245 else:
1246 host = zk_host
1247 port = None
1248
1249 self.zookeeper_host = host
1250
1251 if not port:
1252 self.zookeeper_port = 2181
1253 else:
1254 self.zookeeper_port = int(port)
1255
Clark Boylan621ec9a2017-04-07 17:41:33 -07001256 self.test_id = test_id
1257
James E. Blair498059b2016-12-20 13:50:13 -08001258 def _setUp(self):
1259 # Make sure the test chroot paths do not conflict
1260 random_bits = ''.join(random.choice(string.ascii_lowercase +
1261 string.ascii_uppercase)
1262 for x in range(8))
1263
Clark Boylan621ec9a2017-04-07 17:41:33 -07001264 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001265 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1266
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001267 self.addCleanup(self._cleanup)
1268
James E. Blair498059b2016-12-20 13:50:13 -08001269 # Ensure the chroot path exists and clean up any pre-existing znodes.
1270 _tmp_client = kazoo.client.KazooClient(
1271 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1272 _tmp_client.start()
1273
1274 if _tmp_client.exists(self.zookeeper_chroot):
1275 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1276
1277 _tmp_client.ensure_path(self.zookeeper_chroot)
1278 _tmp_client.stop()
1279 _tmp_client.close()
1280
James E. Blair498059b2016-12-20 13:50:13 -08001281 def _cleanup(self):
1282 '''Remove the chroot path.'''
1283 # Need a non-chroot'ed client to remove the chroot path
1284 _tmp_client = kazoo.client.KazooClient(
1285 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1286 _tmp_client.start()
1287 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1288 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001289 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001290
1291
Joshua Heskethd78b4482015-09-14 16:56:34 -06001292class MySQLSchemaFixture(fixtures.Fixture):
1293 def setUp(self):
1294 super(MySQLSchemaFixture, self).setUp()
1295
1296 random_bits = ''.join(random.choice(string.ascii_lowercase +
1297 string.ascii_uppercase)
1298 for x in range(8))
1299 self.name = '%s_%s' % (random_bits, os.getpid())
1300 self.passwd = uuid.uuid4().hex
1301 db = pymysql.connect(host="localhost",
1302 user="openstack_citest",
1303 passwd="openstack_citest",
1304 db="openstack_citest")
1305 cur = db.cursor()
1306 cur.execute("create database %s" % self.name)
1307 cur.execute(
1308 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1309 (self.name, self.name, self.passwd))
1310 cur.execute("flush privileges")
1311
1312 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1313 self.passwd,
1314 self.name)
1315 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1316 self.addCleanup(self.cleanup)
1317
1318 def cleanup(self):
1319 db = pymysql.connect(host="localhost",
1320 user="openstack_citest",
1321 passwd="openstack_citest",
1322 db="openstack_citest")
1323 cur = db.cursor()
1324 cur.execute("drop database %s" % self.name)
1325 cur.execute("drop user '%s'@'localhost'" % self.name)
1326 cur.execute("flush privileges")
1327
1328
Maru Newby3fe5f852015-01-13 04:22:14 +00001329class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001330 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001331 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001332
James E. Blair1c236df2017-02-01 14:07:24 -08001333 def attachLogs(self, *args):
1334 def reader():
1335 self._log_stream.seek(0)
1336 while True:
1337 x = self._log_stream.read(4096)
1338 if not x:
1339 break
1340 yield x.encode('utf8')
1341 content = testtools.content.content_from_reader(
1342 reader,
1343 testtools.content_type.UTF8_TEXT,
1344 False)
1345 self.addDetail('logging', content)
1346
Clark Boylanb640e052014-04-03 16:41:46 -07001347 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001348 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001349 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1350 try:
1351 test_timeout = int(test_timeout)
1352 except ValueError:
1353 # If timeout value is invalid do not set a timeout.
1354 test_timeout = 0
1355 if test_timeout > 0:
1356 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1357
1358 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1359 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1360 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1361 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1362 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1363 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1364 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1365 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1366 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1367 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001368 self._log_stream = StringIO()
1369 self.addOnException(self.attachLogs)
1370 else:
1371 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001372
James E. Blair1c236df2017-02-01 14:07:24 -08001373 handler = logging.StreamHandler(self._log_stream)
1374 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1375 '%(levelname)-8s %(message)s')
1376 handler.setFormatter(formatter)
1377
1378 logger = logging.getLogger()
1379 logger.setLevel(logging.DEBUG)
1380 logger.addHandler(handler)
1381
Clark Boylan3410d532017-04-25 12:35:29 -07001382 # Make sure we don't carry old handlers around in process state
1383 # which slows down test runs
1384 self.addCleanup(logger.removeHandler, handler)
1385 self.addCleanup(handler.close)
1386 self.addCleanup(handler.flush)
1387
James E. Blair1c236df2017-02-01 14:07:24 -08001388 # NOTE(notmorgan): Extract logging overrides for specific
1389 # libraries from the OS_LOG_DEFAULTS env and create loggers
1390 # for each. This is used to limit the output during test runs
1391 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001392 log_defaults_from_env = os.environ.get(
1393 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001394 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001395
James E. Blairdce6cea2016-12-20 16:45:32 -08001396 if log_defaults_from_env:
1397 for default in log_defaults_from_env.split(','):
1398 try:
1399 name, level_str = default.split('=', 1)
1400 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001401 logger = logging.getLogger(name)
1402 logger.setLevel(level)
1403 logger.addHandler(handler)
1404 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001405 except ValueError:
1406 # NOTE(notmorgan): Invalid format of the log default,
1407 # skip and don't try and apply a logger for the
1408 # specified module
1409 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001410
Maru Newby3fe5f852015-01-13 04:22:14 +00001411
1412class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001413 """A test case with a functioning Zuul.
1414
1415 The following class variables are used during test setup and can
1416 be overidden by subclasses but are effectively read-only once a
1417 test method starts running:
1418
1419 :cvar str config_file: This points to the main zuul config file
1420 within the fixtures directory. Subclasses may override this
1421 to obtain a different behavior.
1422
1423 :cvar str tenant_config_file: This is the tenant config file
1424 (which specifies from what git repos the configuration should
1425 be loaded). It defaults to the value specified in
1426 `config_file` but can be overidden by subclasses to obtain a
1427 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001428 configuration. See also the :py:func:`simple_layout`
1429 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001430
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001431 :cvar bool create_project_keys: Indicates whether Zuul should
1432 auto-generate keys for each project, or whether the test
1433 infrastructure should insert dummy keys to save time during
1434 startup. Defaults to False.
1435
James E. Blaire7b99a02016-08-05 14:27:34 -07001436 The following are instance variables that are useful within test
1437 methods:
1438
1439 :ivar FakeGerritConnection fake_<connection>:
1440 A :py:class:`~tests.base.FakeGerritConnection` will be
1441 instantiated for each connection present in the config file
1442 and stored here. For instance, `fake_gerrit` will hold the
1443 FakeGerritConnection object for a connection named `gerrit`.
1444
1445 :ivar FakeGearmanServer gearman_server: An instance of
1446 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1447 server that all of the Zuul components in this test use to
1448 communicate with each other.
1449
Paul Belanger174a8272017-03-14 13:20:10 -04001450 :ivar RecordingExecutorServer executor_server: An instance of
1451 :py:class:`~tests.base.RecordingExecutorServer` which is the
1452 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001453
1454 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1455 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001456 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001457 list upon completion.
1458
1459 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1460 objects representing completed builds. They are appended to
1461 the list in the order they complete.
1462
1463 """
1464
James E. Blair83005782015-12-11 14:46:03 -08001465 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001466 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001467 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001468
1469 def _startMerger(self):
1470 self.merge_server = zuul.merger.server.MergeServer(self.config,
1471 self.connections)
1472 self.merge_server.start()
1473
Maru Newby3fe5f852015-01-13 04:22:14 +00001474 def setUp(self):
1475 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001476
1477 self.setupZK()
1478
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001479 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001480 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001481 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1482 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001483 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001484 tmp_root = tempfile.mkdtemp(
1485 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001486 self.test_root = os.path.join(tmp_root, "zuul-test")
1487 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001488 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001489 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001490 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001491
1492 if os.path.exists(self.test_root):
1493 shutil.rmtree(self.test_root)
1494 os.makedirs(self.test_root)
1495 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001496 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001497
1498 # Make per test copy of Configuration.
1499 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001500 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001501 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001502 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001503 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001504 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001505 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001506
Clark Boylanb640e052014-04-03 16:41:46 -07001507 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001508 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1509 # see: https://github.com/jsocol/pystatsd/issues/61
1510 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001511 os.environ['STATSD_PORT'] = str(self.statsd.port)
1512 self.statsd.start()
1513 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001514 reload_module(statsd)
1515 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001516
1517 self.gearman_server = FakeGearmanServer()
1518
1519 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001520 self.log.info("Gearman server on port %s" %
1521 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001522
James E. Blaire511d2f2016-12-08 15:22:26 -08001523 gerritsource.GerritSource.replication_timeout = 1.5
1524 gerritsource.GerritSource.replication_retry_interval = 0.5
1525 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001526
Joshua Hesketh352264b2015-08-11 23:42:08 +10001527 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001528
Jan Hruban7083edd2015-08-21 14:00:54 +02001529 self.webapp = zuul.webapp.WebApp(
1530 self.sched, port=0, listen_address='127.0.0.1')
1531
Jan Hruban6b71aff2015-10-22 16:58:08 +02001532 self.event_queues = [
1533 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001534 self.sched.trigger_event_queue,
1535 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001536 ]
1537
James E. Blairfef78942016-03-11 16:28:56 -08001538 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001539 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001540
Clark Boylanb640e052014-04-03 16:41:46 -07001541 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001542 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001543 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001544 return FakeURLOpener(self.upstream_root, *args, **kw)
1545
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001546 old_urlopen = urllib.request.urlopen
1547 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001548
James E. Blair3f876d52016-07-22 13:07:14 -07001549 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001550
Paul Belanger174a8272017-03-14 13:20:10 -04001551 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001552 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001553 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001554 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001555 _test_root=self.test_root,
1556 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001557 self.executor_server.start()
1558 self.history = self.executor_server.build_history
1559 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001560
Paul Belanger174a8272017-03-14 13:20:10 -04001561 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001562 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001563 self.merge_client = zuul.merger.client.MergeClient(
1564 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001565 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001566 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001567 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001568
James E. Blair0d5a36e2017-02-21 10:53:44 -05001569 self.fake_nodepool = FakeNodepool(
1570 self.zk_chroot_fixture.zookeeper_host,
1571 self.zk_chroot_fixture.zookeeper_port,
1572 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001573
Paul Belanger174a8272017-03-14 13:20:10 -04001574 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001575 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001576 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001577 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001578
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001579 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001580
1581 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001582 self.webapp.start()
1583 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001584 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001585 # Cleanups are run in reverse order
1586 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001587 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001588 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001589
James E. Blairb9c0d772017-03-03 14:34:49 -08001590 self.sched.reconfigure(self.config)
1591 self.sched.resume()
1592
James E. Blairfef78942016-03-11 16:28:56 -08001593 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001594 # Set up gerrit related fakes
1595 # Set a changes database so multiple FakeGerrit's can report back to
1596 # a virtual canonical database given by the configured hostname
1597 self.gerrit_changes_dbs = {}
1598
1599 def getGerritConnection(driver, name, config):
1600 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1601 con = FakeGerritConnection(driver, name, config,
1602 changes_db=db,
1603 upstream_root=self.upstream_root)
1604 self.event_queues.append(con.event_queue)
1605 setattr(self, 'fake_' + name, con)
1606 return con
1607
1608 self.useFixture(fixtures.MonkeyPatch(
1609 'zuul.driver.gerrit.GerritDriver.getConnection',
1610 getGerritConnection))
1611
Gregory Haynes4fc12542015-04-22 20:38:06 -07001612 def getGithubConnection(driver, name, config):
1613 con = FakeGithubConnection(driver, name, config,
1614 upstream_root=self.upstream_root)
1615 setattr(self, 'fake_' + name, con)
1616 return con
1617
1618 self.useFixture(fixtures.MonkeyPatch(
1619 'zuul.driver.github.GithubDriver.getConnection',
1620 getGithubConnection))
1621
James E. Blaire511d2f2016-12-08 15:22:26 -08001622 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001623 # TODO(jhesketh): This should come from lib.connections for better
1624 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001625 # Register connections from the config
1626 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001627
Joshua Hesketh352264b2015-08-11 23:42:08 +10001628 def FakeSMTPFactory(*args, **kw):
1629 args = [self.smtp_messages] + list(args)
1630 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001631
Joshua Hesketh352264b2015-08-11 23:42:08 +10001632 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001633
James E. Blaire511d2f2016-12-08 15:22:26 -08001634 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001635 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001636 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001637
James E. Blair83005782015-12-11 14:46:03 -08001638 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001639 # This creates the per-test configuration object. It can be
1640 # overriden by subclasses, but should not need to be since it
1641 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001642 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001643 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001644
1645 if not self.setupSimpleLayout():
1646 if hasattr(self, 'tenant_config_file'):
1647 self.config.set('zuul', 'tenant_config',
1648 self.tenant_config_file)
1649 git_path = os.path.join(
1650 os.path.dirname(
1651 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1652 'git')
1653 if os.path.exists(git_path):
1654 for reponame in os.listdir(git_path):
1655 project = reponame.replace('_', '/')
1656 self.copyDirToRepo(project,
1657 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001658 self.setupAllProjectKeys()
1659
James E. Blair06cc3922017-04-19 10:08:10 -07001660 def setupSimpleLayout(self):
1661 # If the test method has been decorated with a simple_layout,
1662 # use that instead of the class tenant_config_file. Set up a
1663 # single config-project with the specified layout, and
1664 # initialize repos for all of the 'project' entries which
1665 # appear in the layout.
1666 test_name = self.id().split('.')[-1]
1667 test = getattr(self, test_name)
1668 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001669 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001670 else:
1671 return False
1672
James E. Blairb70e55a2017-04-19 12:57:02 -07001673 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001674 path = os.path.join(FIXTURE_DIR, path)
1675 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001676 data = f.read()
1677 layout = yaml.safe_load(data)
1678 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001679 untrusted_projects = []
1680 for item in layout:
1681 if 'project' in item:
1682 name = item['project']['name']
1683 untrusted_projects.append(name)
1684 self.init_repo(name)
1685 self.addCommitToRepo(name, 'initial commit',
1686 files={'README': ''},
1687 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001688 if 'job' in item:
1689 jobname = item['job']['name']
1690 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001691
1692 root = os.path.join(self.test_root, "config")
1693 if not os.path.exists(root):
1694 os.makedirs(root)
1695 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1696 config = [{'tenant':
1697 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001698 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001699 {'config-projects': ['common-config'],
1700 'untrusted-projects': untrusted_projects}}}}]
1701 f.write(yaml.dump(config))
1702 f.close()
1703 self.config.set('zuul', 'tenant_config',
1704 os.path.join(FIXTURE_DIR, f.name))
1705
1706 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001707 self.addCommitToRepo('common-config', 'add content from fixture',
1708 files, branch='master', tag='init')
1709
1710 return True
1711
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001712 def setupAllProjectKeys(self):
1713 if self.create_project_keys:
1714 return
1715
1716 path = self.config.get('zuul', 'tenant_config')
1717 with open(os.path.join(FIXTURE_DIR, path)) as f:
1718 tenant_config = yaml.safe_load(f.read())
1719 for tenant in tenant_config:
1720 sources = tenant['tenant']['source']
1721 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001722 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001723 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001724 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001725 self.setupProjectKeys(source, project)
1726
1727 def setupProjectKeys(self, source, project):
1728 # Make sure we set up an RSA key for the project so that we
1729 # don't spend time generating one:
1730
1731 key_root = os.path.join(self.state_root, 'keys')
1732 if not os.path.isdir(key_root):
1733 os.mkdir(key_root, 0o700)
1734 private_key_file = os.path.join(key_root, source, project + '.pem')
1735 private_key_dir = os.path.dirname(private_key_file)
1736 self.log.debug("Installing test keys for project %s at %s" % (
1737 project, private_key_file))
1738 if not os.path.isdir(private_key_dir):
1739 os.makedirs(private_key_dir)
1740 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1741 with open(private_key_file, 'w') as o:
1742 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001743
James E. Blair498059b2016-12-20 13:50:13 -08001744 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001745 self.zk_chroot_fixture = self.useFixture(
1746 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001747 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001748 self.zk_chroot_fixture.zookeeper_host,
1749 self.zk_chroot_fixture.zookeeper_port,
1750 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001751
James E. Blair96c6bf82016-01-15 16:20:40 -08001752 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001753 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001754
1755 files = {}
1756 for (dirpath, dirnames, filenames) in os.walk(source_path):
1757 for filename in filenames:
1758 test_tree_filepath = os.path.join(dirpath, filename)
1759 common_path = os.path.commonprefix([test_tree_filepath,
1760 source_path])
1761 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1762 with open(test_tree_filepath, 'r') as f:
1763 content = f.read()
1764 files[relative_filepath] = content
1765 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001766 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001767
James E. Blaire18d4602017-01-05 11:17:28 -08001768 def assertNodepoolState(self):
1769 # Make sure that there are no pending requests
1770
1771 requests = self.fake_nodepool.getNodeRequests()
1772 self.assertEqual(len(requests), 0)
1773
1774 nodes = self.fake_nodepool.getNodes()
1775 for node in nodes:
1776 self.assertFalse(node['_lock'], "Node %s is locked" %
1777 (node['_oid'],))
1778
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001779 def assertNoGeneratedKeys(self):
1780 # Make sure that Zuul did not generate any project keys
1781 # (unless it was supposed to).
1782
1783 if self.create_project_keys:
1784 return
1785
1786 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1787 test_key = i.read()
1788
1789 key_root = os.path.join(self.state_root, 'keys')
1790 for root, dirname, files in os.walk(key_root):
1791 for fn in files:
1792 with open(os.path.join(root, fn)) as f:
1793 self.assertEqual(test_key, f.read())
1794
Clark Boylanb640e052014-04-03 16:41:46 -07001795 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001796 self.log.debug("Assert final state")
1797 # Make sure no jobs are running
1798 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001799 # Make sure that git.Repo objects have been garbage collected.
1800 repos = []
1801 gc.collect()
1802 for obj in gc.get_objects():
1803 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001804 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001805 repos.append(obj)
1806 self.assertEqual(len(repos), 0)
1807 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001808 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001809 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001810 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001811 for tenant in self.sched.abide.tenants.values():
1812 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001813 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001814 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001815
1816 def shutdown(self):
1817 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001818 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001819 self.merge_server.stop()
1820 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001821 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001822 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001823 self.sched.stop()
1824 self.sched.join()
1825 self.statsd.stop()
1826 self.statsd.join()
1827 self.webapp.stop()
1828 self.webapp.join()
1829 self.rpc.stop()
1830 self.rpc.join()
1831 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001832 self.fake_nodepool.stop()
1833 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001834 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001835 # we whitelist watchdog threads as they have relatively long delays
1836 # before noticing they should exit, but they should exit on their own.
1837 threads = [t for t in threading.enumerate()
1838 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001839 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001840 log_str = ""
1841 for thread_id, stack_frame in sys._current_frames().items():
1842 log_str += "Thread: %s\n" % thread_id
1843 log_str += "".join(traceback.format_stack(stack_frame))
1844 self.log.debug(log_str)
1845 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001846
James E. Blaira002b032017-04-18 10:35:48 -07001847 def assertCleanShutdown(self):
1848 pass
1849
James E. Blairc4ba97a2017-04-19 16:26:24 -07001850 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001851 parts = project.split('/')
1852 path = os.path.join(self.upstream_root, *parts[:-1])
1853 if not os.path.exists(path):
1854 os.makedirs(path)
1855 path = os.path.join(self.upstream_root, project)
1856 repo = git.Repo.init(path)
1857
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001858 with repo.config_writer() as config_writer:
1859 config_writer.set_value('user', 'email', 'user@example.com')
1860 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001861
Clark Boylanb640e052014-04-03 16:41:46 -07001862 repo.index.commit('initial commit')
1863 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001864 if tag:
1865 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001866
James E. Blair97d902e2014-08-21 13:25:56 -07001867 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001868 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001869 repo.git.clean('-x', '-f', '-d')
1870
James E. Blair97d902e2014-08-21 13:25:56 -07001871 def create_branch(self, project, branch):
1872 path = os.path.join(self.upstream_root, project)
1873 repo = git.Repo.init(path)
1874 fn = os.path.join(path, 'README')
1875
1876 branch_head = repo.create_head(branch)
1877 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001878 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001879 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001880 f.close()
1881 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001882 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001883
James E. Blair97d902e2014-08-21 13:25:56 -07001884 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001885 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001886 repo.git.clean('-x', '-f', '-d')
1887
Sachi King9f16d522016-03-16 12:20:45 +11001888 def create_commit(self, project):
1889 path = os.path.join(self.upstream_root, project)
1890 repo = git.Repo(path)
1891 repo.head.reference = repo.heads['master']
1892 file_name = os.path.join(path, 'README')
1893 with open(file_name, 'a') as f:
1894 f.write('creating fake commit\n')
1895 repo.index.add([file_name])
1896 commit = repo.index.commit('Creating a fake commit')
1897 return commit.hexsha
1898
James E. Blairf4a5f022017-04-18 14:01:10 -07001899 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001900 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001901 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001902 while len(self.builds):
1903 self.release(self.builds[0])
1904 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001905 i += 1
1906 if count is not None and i >= count:
1907 break
James E. Blairb8c16472015-05-05 14:55:26 -07001908
Clark Boylanb640e052014-04-03 16:41:46 -07001909 def release(self, job):
1910 if isinstance(job, FakeBuild):
1911 job.release()
1912 else:
1913 job.waiting = False
1914 self.log.debug("Queued job %s released" % job.unique)
1915 self.gearman_server.wakeConnections()
1916
1917 def getParameter(self, job, name):
1918 if isinstance(job, FakeBuild):
1919 return job.parameters[name]
1920 else:
1921 parameters = json.loads(job.arguments)
1922 return parameters[name]
1923
Clark Boylanb640e052014-04-03 16:41:46 -07001924 def haveAllBuildsReported(self):
1925 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001926 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001927 return False
1928 # Find out if every build that the worker has completed has been
1929 # reported back to Zuul. If it hasn't then that means a Gearman
1930 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001931 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001932 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001933 if not zbuild:
1934 # It has already been reported
1935 continue
1936 # It hasn't been reported yet.
1937 return False
1938 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001939 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001940 if connection.state == 'GRAB_WAIT':
1941 return False
1942 return True
1943
1944 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001945 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001946 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001947 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001948 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001949 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001950 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001951 for j in conn.related_jobs.values():
1952 if j.unique == build.uuid:
1953 client_job = j
1954 break
1955 if not client_job:
1956 self.log.debug("%s is not known to the gearman client" %
1957 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001958 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001959 if not client_job.handle:
1960 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001961 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001962 server_job = self.gearman_server.jobs.get(client_job.handle)
1963 if not server_job:
1964 self.log.debug("%s is not known to the gearman server" %
1965 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001966 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001967 if not hasattr(server_job, 'waiting'):
1968 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001969 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001970 if server_job.waiting:
1971 continue
James E. Blair17302972016-08-10 16:11:42 -07001972 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001973 self.log.debug("%s has not reported start" % build)
1974 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001975 worker_build = self.executor_server.job_builds.get(
1976 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001977 if worker_build:
1978 if worker_build.isWaiting():
1979 continue
1980 else:
1981 self.log.debug("%s is running" % worker_build)
1982 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001983 else:
James E. Blair962220f2016-08-03 11:22:38 -07001984 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001985 return False
James E. Blaira002b032017-04-18 10:35:48 -07001986 for (build_uuid, job_worker) in \
1987 self.executor_server.job_workers.items():
1988 if build_uuid not in seen_builds:
1989 self.log.debug("%s is not finalized" % build_uuid)
1990 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001991 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001992
James E. Blairdce6cea2016-12-20 16:45:32 -08001993 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001994 if self.fake_nodepool.paused:
1995 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08001996 if self.sched.nodepool.requests:
1997 return False
1998 return True
1999
Jan Hruban6b71aff2015-10-22 16:58:08 +02002000 def eventQueuesEmpty(self):
2001 for queue in self.event_queues:
2002 yield queue.empty()
2003
2004 def eventQueuesJoin(self):
2005 for queue in self.event_queues:
2006 queue.join()
2007
Clark Boylanb640e052014-04-03 16:41:46 -07002008 def waitUntilSettled(self):
2009 self.log.debug("Waiting until settled...")
2010 start = time.time()
2011 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002012 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002013 self.log.error("Timeout waiting for Zuul to settle")
2014 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002015 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002016 self.log.error(" %s: %s" % (queue, queue.empty()))
2017 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002018 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002019 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002020 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002021 self.log.error("All requests completed: %s" %
2022 (self.areAllNodeRequestsComplete(),))
2023 self.log.error("Merge client jobs: %s" %
2024 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002025 raise Exception("Timeout waiting for Zuul to settle")
2026 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002027
Paul Belanger174a8272017-03-14 13:20:10 -04002028 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002029 # have all build states propogated to zuul?
2030 if self.haveAllBuildsReported():
2031 # Join ensures that the queue is empty _and_ events have been
2032 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002033 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002034 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002035 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002036 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002037 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002038 self.areAllNodeRequestsComplete() and
2039 all(self.eventQueuesEmpty())):
2040 # The queue empty check is placed at the end to
2041 # ensure that if a component adds an event between
2042 # when locked the run handler and checked that the
2043 # components were stable, we don't erroneously
2044 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002045 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002046 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002047 self.log.debug("...settled.")
2048 return
2049 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002050 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002051 self.sched.wake_event.wait(0.1)
2052
2053 def countJobResults(self, jobs, result):
2054 jobs = filter(lambda x: x.result == result, jobs)
2055 return len(jobs)
2056
James E. Blair96c6bf82016-01-15 16:20:40 -08002057 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002058 for job in self.history:
2059 if (job.name == name and
2060 (project is None or
2061 job.parameters['ZUUL_PROJECT'] == project)):
2062 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002063 raise Exception("Unable to find job %s in history" % name)
2064
2065 def assertEmptyQueues(self):
2066 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002067 for tenant in self.sched.abide.tenants.values():
2068 for pipeline in tenant.layout.pipelines.values():
2069 for queue in pipeline.queues:
2070 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002071 print('pipeline %s queue %s contents %s' % (
2072 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002073 self.assertEqual(len(queue.queue), 0,
2074 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002075
2076 def assertReportedStat(self, key, value=None, kind=None):
2077 start = time.time()
2078 while time.time() < (start + 5):
2079 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002080 k, v = stat.split(':')
2081 if key == k:
2082 if value is None and kind is None:
2083 return
2084 elif value:
2085 if value == v:
2086 return
2087 elif kind:
2088 if v.endswith('|' + kind):
2089 return
2090 time.sleep(0.1)
2091
Clark Boylanb640e052014-04-03 16:41:46 -07002092 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002093
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002094 def assertBuilds(self, builds):
2095 """Assert that the running builds are as described.
2096
2097 The list of running builds is examined and must match exactly
2098 the list of builds described by the input.
2099
2100 :arg list builds: A list of dictionaries. Each item in the
2101 list must match the corresponding build in the build
2102 history, and each element of the dictionary must match the
2103 corresponding attribute of the build.
2104
2105 """
James E. Blair3158e282016-08-19 09:34:11 -07002106 try:
2107 self.assertEqual(len(self.builds), len(builds))
2108 for i, d in enumerate(builds):
2109 for k, v in d.items():
2110 self.assertEqual(
2111 getattr(self.builds[i], k), v,
2112 "Element %i in builds does not match" % (i,))
2113 except Exception:
2114 for build in self.builds:
2115 self.log.error("Running build: %s" % build)
2116 else:
2117 self.log.error("No running builds")
2118 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002119
James E. Blairb536ecc2016-08-31 10:11:42 -07002120 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002121 """Assert that the completed builds are as described.
2122
2123 The list of completed builds is examined and must match
2124 exactly the list of builds described by the input.
2125
2126 :arg list history: A list of dictionaries. Each item in the
2127 list must match the corresponding build in the build
2128 history, and each element of the dictionary must match the
2129 corresponding attribute of the build.
2130
James E. Blairb536ecc2016-08-31 10:11:42 -07002131 :arg bool ordered: If true, the history must match the order
2132 supplied, if false, the builds are permitted to have
2133 arrived in any order.
2134
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002135 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002136 def matches(history_item, item):
2137 for k, v in item.items():
2138 if getattr(history_item, k) != v:
2139 return False
2140 return True
James E. Blair3158e282016-08-19 09:34:11 -07002141 try:
2142 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002143 if ordered:
2144 for i, d in enumerate(history):
2145 if not matches(self.history[i], d):
2146 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002147 "Element %i in history does not match %s" %
2148 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002149 else:
2150 unseen = self.history[:]
2151 for i, d in enumerate(history):
2152 found = False
2153 for unseen_item in unseen:
2154 if matches(unseen_item, d):
2155 found = True
2156 unseen.remove(unseen_item)
2157 break
2158 if not found:
2159 raise Exception("No match found for element %i "
2160 "in history" % (i,))
2161 if unseen:
2162 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002163 except Exception:
2164 for build in self.history:
2165 self.log.error("Completed build: %s" % build)
2166 else:
2167 self.log.error("No completed builds")
2168 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002169
James E. Blair6ac368c2016-12-22 18:07:20 -08002170 def printHistory(self):
2171 """Log the build history.
2172
2173 This can be useful during tests to summarize what jobs have
2174 completed.
2175
2176 """
2177 self.log.debug("Build history:")
2178 for build in self.history:
2179 self.log.debug(build)
2180
James E. Blair59fdbac2015-12-07 17:08:06 -08002181 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002182 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2183
James E. Blair9ea70072017-04-19 16:05:30 -07002184 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002185 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002186 if not os.path.exists(root):
2187 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002188 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2189 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002190- tenant:
2191 name: openstack
2192 source:
2193 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002194 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002195 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002196 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002197 - org/project
2198 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002199 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002200 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002201 self.config.set('zuul', 'tenant_config',
2202 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002203 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002204
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002205 def addCommitToRepo(self, project, message, files,
2206 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002207 path = os.path.join(self.upstream_root, project)
2208 repo = git.Repo(path)
2209 repo.head.reference = branch
2210 zuul.merger.merger.reset_repo_to_head(repo)
2211 for fn, content in files.items():
2212 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002213 try:
2214 os.makedirs(os.path.dirname(fn))
2215 except OSError:
2216 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002217 with open(fn, 'w') as f:
2218 f.write(content)
2219 repo.index.add([fn])
2220 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002221 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002222 repo.heads[branch].commit = commit
2223 repo.head.reference = branch
2224 repo.git.clean('-x', '-f', '-d')
2225 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002226 if tag:
2227 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002228 return before
2229
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002230 def commitConfigUpdate(self, project_name, source_name):
2231 """Commit an update to zuul.yaml
2232
2233 This overwrites the zuul.yaml in the specificed project with
2234 the contents specified.
2235
2236 :arg str project_name: The name of the project containing
2237 zuul.yaml (e.g., common-config)
2238
2239 :arg str source_name: The path to the file (underneath the
2240 test fixture directory) whose contents should be used to
2241 replace zuul.yaml.
2242 """
2243
2244 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002245 files = {}
2246 with open(source_path, 'r') as f:
2247 data = f.read()
2248 layout = yaml.safe_load(data)
2249 files['zuul.yaml'] = data
2250 for item in layout:
2251 if 'job' in item:
2252 jobname = item['job']['name']
2253 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002254 before = self.addCommitToRepo(
2255 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002256 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002257 return before
2258
James E. Blair7fc8daa2016-08-08 15:37:15 -07002259 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002260
James E. Blair7fc8daa2016-08-08 15:37:15 -07002261 """Inject a Fake (Gerrit) event.
2262
2263 This method accepts a JSON-encoded event and simulates Zuul
2264 having received it from Gerrit. It could (and should)
2265 eventually apply to any connection type, but is currently only
2266 used with Gerrit connections. The name of the connection is
2267 used to look up the corresponding server, and the event is
2268 simulated as having been received by all Zuul connections
2269 attached to that server. So if two Gerrit connections in Zuul
2270 are connected to the same Gerrit server, and you invoke this
2271 method specifying the name of one of them, the event will be
2272 received by both.
2273
2274 .. note::
2275
2276 "self.fake_gerrit.addEvent" calls should be migrated to
2277 this method.
2278
2279 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002280 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002281 :arg str event: The JSON-encoded event.
2282
2283 """
2284 specified_conn = self.connections.connections[connection]
2285 for conn in self.connections.connections.values():
2286 if (isinstance(conn, specified_conn.__class__) and
2287 specified_conn.server == conn.server):
2288 conn.addEvent(event)
2289
James E. Blair3f876d52016-07-22 13:07:14 -07002290
2291class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002292 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002293 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002294
Joshua Heskethd78b4482015-09-14 16:56:34 -06002295
2296class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002297 def setup_config(self):
2298 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002299 for section_name in self.config.sections():
2300 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2301 section_name, re.I)
2302 if not con_match:
2303 continue
2304
2305 if self.config.get(section_name, 'driver') == 'sql':
2306 f = MySQLSchemaFixture()
2307 self.useFixture(f)
2308 if (self.config.get(section_name, 'dburi') ==
2309 '$MYSQL_FIXTURE_DBURI$'):
2310 self.config.set(section_name, 'dburi', f.dburi)