blob: d6304d7fed02f84ceb446272095ba381d4669dbc [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
Wayne40f40042015-06-12 16:56:30 -0700696 def report(self, project, pr_number, message, params=None):
697 pull_request = self.pull_requests[pr_number - 1]
698 pull_request.addComment(message)
699
Gregory Haynes4fc12542015-04-22 20:38:06 -0700700
Clark Boylanb640e052014-04-03 16:41:46 -0700701class BuildHistory(object):
702 def __init__(self, **kw):
703 self.__dict__.update(kw)
704
705 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700706 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
707 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700708
709
710class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200711 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700712 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700713 self.url = url
714
715 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700716 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700717 path = res.path
718 project = '/'.join(path.split('/')[2:-2])
719 ret = '001e# service=git-upload-pack\n'
720 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
721 'multi_ack thin-pack side-band side-band-64k ofs-delta '
722 'shallow no-progress include-tag multi_ack_detailed no-done\n')
723 path = os.path.join(self.upstream_root, project)
724 repo = git.Repo(path)
725 for ref in repo.refs:
726 r = ref.object.hexsha + ' ' + ref.path + '\n'
727 ret += '%04x%s' % (len(r) + 4, r)
728 ret += '0000'
729 return ret
730
731
Clark Boylanb640e052014-04-03 16:41:46 -0700732class FakeStatsd(threading.Thread):
733 def __init__(self):
734 threading.Thread.__init__(self)
735 self.daemon = True
736 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
737 self.sock.bind(('', 0))
738 self.port = self.sock.getsockname()[1]
739 self.wake_read, self.wake_write = os.pipe()
740 self.stats = []
741
742 def run(self):
743 while True:
744 poll = select.poll()
745 poll.register(self.sock, select.POLLIN)
746 poll.register(self.wake_read, select.POLLIN)
747 ret = poll.poll()
748 for (fd, event) in ret:
749 if fd == self.sock.fileno():
750 data = self.sock.recvfrom(1024)
751 if not data:
752 return
753 self.stats.append(data[0])
754 if fd == self.wake_read:
755 return
756
757 def stop(self):
758 os.write(self.wake_write, '1\n')
759
760
James E. Blaire1767bc2016-08-02 10:00:27 -0700761class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700762 log = logging.getLogger("zuul.test")
763
Paul Belanger174a8272017-03-14 13:20:10 -0400764 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700765 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400766 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700767 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700768 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700769 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700770 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700771 # TODOv3(jeblair): self.node is really "the image of the node
772 # assigned". We should rename it (self.node_image?) if we
773 # keep using it like this, or we may end up exposing more of
774 # the complexity around multi-node jobs here
775 # (self.nodes[0].image?)
776 self.node = None
777 if len(self.parameters.get('nodes')) == 1:
778 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700779 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100780 self.pipeline = self.parameters['ZUUL_PIPELINE']
781 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700782 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700783 self.wait_condition = threading.Condition()
784 self.waiting = False
785 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500786 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700787 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700788 self.changes = None
789 if 'ZUUL_CHANGE_IDS' in self.parameters:
790 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700791
James E. Blair3158e282016-08-19 09:34:11 -0700792 def __repr__(self):
793 waiting = ''
794 if self.waiting:
795 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100796 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
797 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700798
Clark Boylanb640e052014-04-03 16:41:46 -0700799 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700800 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700801 self.wait_condition.acquire()
802 self.wait_condition.notify()
803 self.waiting = False
804 self.log.debug("Build %s released" % self.unique)
805 self.wait_condition.release()
806
807 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700808 """Return whether this build is being held.
809
810 :returns: Whether the build is being held.
811 :rtype: bool
812 """
813
Clark Boylanb640e052014-04-03 16:41:46 -0700814 self.wait_condition.acquire()
815 if self.waiting:
816 ret = True
817 else:
818 ret = False
819 self.wait_condition.release()
820 return ret
821
822 def _wait(self):
823 self.wait_condition.acquire()
824 self.waiting = True
825 self.log.debug("Build %s waiting" % self.unique)
826 self.wait_condition.wait()
827 self.wait_condition.release()
828
829 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700830 self.log.debug('Running build %s' % self.unique)
831
Paul Belanger174a8272017-03-14 13:20:10 -0400832 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700833 self.log.debug('Holding build %s' % self.unique)
834 self._wait()
835 self.log.debug("Build %s continuing" % self.unique)
836
James E. Blair412fba82017-01-26 15:00:50 -0800837 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700838 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800839 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700840 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800841 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500842 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800843 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700844
James E. Blaire1767bc2016-08-02 10:00:27 -0700845 return result
Clark Boylanb640e052014-04-03 16:41:46 -0700846
James E. Blaira5dba232016-08-08 15:53:24 -0700847 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -0400848 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -0700849 for change in changes:
850 if self.hasChanges(change):
851 return True
852 return False
853
James E. Blaire7b99a02016-08-05 14:27:34 -0700854 def hasChanges(self, *changes):
855 """Return whether this build has certain changes in its git repos.
856
857 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -0700858 are expected to be present (in order) in the git repository of
859 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -0700860
861 :returns: Whether the build has the indicated changes.
862 :rtype: bool
863
864 """
Clint Byrum3343e3e2016-11-15 16:05:03 -0800865 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -0700867 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -0800868 try:
869 repo = git.Repo(path)
870 except NoSuchPathError as e:
871 self.log.debug('%s' % e)
872 return False
873 ref = self.parameters['ZUUL_REF']
874 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
875 commit_message = '%s-1' % change.subject
876 self.log.debug("Checking if build %s has changes; commit_message "
877 "%s; repo_messages %s" % (self, commit_message,
878 repo_messages))
879 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -0700880 self.log.debug(" messages do not match")
881 return False
882 self.log.debug(" OK")
883 return True
884
Clark Boylanb640e052014-04-03 16:41:46 -0700885
Paul Belanger174a8272017-03-14 13:20:10 -0400886class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
887 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -0700888
Paul Belanger174a8272017-03-14 13:20:10 -0400889 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -0700890 they will report that they have started but then pause until
891 released before reporting completion. This attribute may be
892 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -0400893 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -0700894 be explicitly released.
895
896 """
James E. Blairf5dbd002015-12-23 15:26:17 -0800897 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -0700898 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -0800899 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -0400900 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -0700901 self.hold_jobs_in_build = False
902 self.lock = threading.Lock()
903 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -0700904 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -0700905 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -0700906 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -0800907
James E. Blaira5dba232016-08-08 15:53:24 -0700908 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -0400909 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -0700910
911 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -0700912 :arg Change change: The :py:class:`~tests.base.FakeChange`
913 instance which should cause the job to fail. This job
914 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -0700915
916 """
James E. Blaire1767bc2016-08-02 10:00:27 -0700917 l = self.fail_tests.get(name, [])
918 l.append(change)
919 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -0800920
James E. Blair962220f2016-08-03 11:22:38 -0700921 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700922 """Release a held build.
923
924 :arg str regex: A regular expression which, if supplied, will
925 cause only builds with matching names to be released. If
926 not supplied, all builds will be released.
927
928 """
James E. Blair962220f2016-08-03 11:22:38 -0700929 builds = self.running_builds[:]
930 self.log.debug("Releasing build %s (%s)" % (regex,
931 len(self.running_builds)))
932 for build in builds:
933 if not regex or re.match(regex, build.name):
934 self.log.debug("Releasing build %s" %
935 (build.parameters['ZUUL_UUID']))
936 build.release()
937 else:
938 self.log.debug("Not releasing build %s" %
939 (build.parameters['ZUUL_UUID']))
940 self.log.debug("Done releasing builds %s (%s)" %
941 (regex, len(self.running_builds)))
942
Paul Belanger174a8272017-03-14 13:20:10 -0400943 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -0700944 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -0700945 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -0700946 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -0700947 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -0800948 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -0500949 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -0800950 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +1100951 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
952 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -0700953
954 def stopJob(self, job):
955 self.log.debug("handle stop")
956 parameters = json.loads(job.arguments)
957 uuid = parameters['uuid']
958 for build in self.running_builds:
959 if build.unique == uuid:
960 build.aborted = True
961 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -0400962 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -0700963
James E. Blaira002b032017-04-18 10:35:48 -0700964 def stop(self):
965 for build in self.running_builds:
966 build.release()
967 super(RecordingExecutorServer, self).stop()
968
Joshua Hesketh50c21782016-10-13 21:34:14 +1100969
Paul Belanger174a8272017-03-14 13:20:10 -0400970class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700971 def doMergeChanges(self, items):
972 # Get a merger in order to update the repos involved in this job.
973 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
974 if not commit: # merge conflict
975 self.recordResult('MERGER_FAILURE')
976 return commit
977
978 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -0400979 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -0400980 self.executor_server.lock.acquire()
981 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -0700982 BuildHistory(name=build.name, result=result, changes=build.changes,
983 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -0800984 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -0700985 pipeline=build.parameters['ZUUL_PIPELINE'])
986 )
Paul Belanger174a8272017-03-14 13:20:10 -0400987 self.executor_server.running_builds.remove(build)
988 del self.executor_server.job_builds[self.job.unique]
989 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -0700990
991 def runPlaybooks(self, args):
992 build = self.executor_server.job_builds[self.job.unique]
993 build.jobdir = self.jobdir
994
995 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
996 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -0800997 return result
998
Monty Taylore6562aa2017-02-20 07:37:39 -0500999 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001000 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001001
Paul Belanger174a8272017-03-14 13:20:10 -04001002 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001003 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001004 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001005 else:
1006 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001007 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001008
James E. Blairad8dca02017-02-21 11:48:32 -05001009 def getHostList(self, args):
1010 self.log.debug("hostlist")
1011 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001012 for host in hosts:
1013 host['host_vars']['ansible_connection'] = 'local'
1014
1015 hosts.append(dict(
1016 name='localhost',
1017 host_vars=dict(ansible_connection='local'),
1018 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001019 return hosts
1020
James E. Blairf5dbd002015-12-23 15:26:17 -08001021
Clark Boylanb640e052014-04-03 16:41:46 -07001022class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001023 """A Gearman server for use in tests.
1024
1025 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1026 added to the queue but will not be distributed to workers
1027 until released. This attribute may be changed at any time and
1028 will take effect for subsequently enqueued jobs, but
1029 previously held jobs will still need to be explicitly
1030 released.
1031
1032 """
1033
Clark Boylanb640e052014-04-03 16:41:46 -07001034 def __init__(self):
1035 self.hold_jobs_in_queue = False
1036 super(FakeGearmanServer, self).__init__(0)
1037
1038 def getJobForConnection(self, connection, peek=False):
1039 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1040 for job in queue:
1041 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001042 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001043 job.waiting = self.hold_jobs_in_queue
1044 else:
1045 job.waiting = False
1046 if job.waiting:
1047 continue
1048 if job.name in connection.functions:
1049 if not peek:
1050 queue.remove(job)
1051 connection.related_jobs[job.handle] = job
1052 job.worker_connection = connection
1053 job.running = True
1054 return job
1055 return None
1056
1057 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001058 """Release a held job.
1059
1060 :arg str regex: A regular expression which, if supplied, will
1061 cause only jobs with matching names to be released. If
1062 not supplied, all jobs will be released.
1063 """
Clark Boylanb640e052014-04-03 16:41:46 -07001064 released = False
1065 qlen = (len(self.high_queue) + len(self.normal_queue) +
1066 len(self.low_queue))
1067 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1068 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001069 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001070 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001071 parameters = json.loads(job.arguments)
1072 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001073 self.log.debug("releasing queued job %s" %
1074 job.unique)
1075 job.waiting = False
1076 released = True
1077 else:
1078 self.log.debug("not releasing queued job %s" %
1079 job.unique)
1080 if released:
1081 self.wakeConnections()
1082 qlen = (len(self.high_queue) + len(self.normal_queue) +
1083 len(self.low_queue))
1084 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1085
1086
1087class FakeSMTP(object):
1088 log = logging.getLogger('zuul.FakeSMTP')
1089
1090 def __init__(self, messages, server, port):
1091 self.server = server
1092 self.port = port
1093 self.messages = messages
1094
1095 def sendmail(self, from_email, to_email, msg):
1096 self.log.info("Sending email from %s, to %s, with msg %s" % (
1097 from_email, to_email, msg))
1098
1099 headers = msg.split('\n\n', 1)[0]
1100 body = msg.split('\n\n', 1)[1]
1101
1102 self.messages.append(dict(
1103 from_email=from_email,
1104 to_email=to_email,
1105 msg=msg,
1106 headers=headers,
1107 body=body,
1108 ))
1109
1110 return True
1111
1112 def quit(self):
1113 return True
1114
1115
James E. Blairdce6cea2016-12-20 16:45:32 -08001116class FakeNodepool(object):
1117 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001118 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001119
1120 log = logging.getLogger("zuul.test.FakeNodepool")
1121
1122 def __init__(self, host, port, chroot):
1123 self.client = kazoo.client.KazooClient(
1124 hosts='%s:%s%s' % (host, port, chroot))
1125 self.client.start()
1126 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001127 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001128 self.thread = threading.Thread(target=self.run)
1129 self.thread.daemon = True
1130 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001131 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001132
1133 def stop(self):
1134 self._running = False
1135 self.thread.join()
1136 self.client.stop()
1137 self.client.close()
1138
1139 def run(self):
1140 while self._running:
1141 self._run()
1142 time.sleep(0.1)
1143
1144 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001145 if self.paused:
1146 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001147 for req in self.getNodeRequests():
1148 self.fulfillRequest(req)
1149
1150 def getNodeRequests(self):
1151 try:
1152 reqids = self.client.get_children(self.REQUEST_ROOT)
1153 except kazoo.exceptions.NoNodeError:
1154 return []
1155 reqs = []
1156 for oid in sorted(reqids):
1157 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001158 try:
1159 data, stat = self.client.get(path)
1160 data = json.loads(data)
1161 data['_oid'] = oid
1162 reqs.append(data)
1163 except kazoo.exceptions.NoNodeError:
1164 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001165 return reqs
1166
James E. Blaire18d4602017-01-05 11:17:28 -08001167 def getNodes(self):
1168 try:
1169 nodeids = self.client.get_children(self.NODE_ROOT)
1170 except kazoo.exceptions.NoNodeError:
1171 return []
1172 nodes = []
1173 for oid in sorted(nodeids):
1174 path = self.NODE_ROOT + '/' + oid
1175 data, stat = self.client.get(path)
1176 data = json.loads(data)
1177 data['_oid'] = oid
1178 try:
1179 lockfiles = self.client.get_children(path + '/lock')
1180 except kazoo.exceptions.NoNodeError:
1181 lockfiles = []
1182 if lockfiles:
1183 data['_lock'] = True
1184 else:
1185 data['_lock'] = False
1186 nodes.append(data)
1187 return nodes
1188
James E. Blaira38c28e2017-01-04 10:33:20 -08001189 def makeNode(self, request_id, node_type):
1190 now = time.time()
1191 path = '/nodepool/nodes/'
1192 data = dict(type=node_type,
1193 provider='test-provider',
1194 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001195 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001196 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001197 public_ipv4='127.0.0.1',
1198 private_ipv4=None,
1199 public_ipv6=None,
1200 allocated_to=request_id,
1201 state='ready',
1202 state_time=now,
1203 created_time=now,
1204 updated_time=now,
1205 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001206 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001207 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001208 data = json.dumps(data)
1209 path = self.client.create(path, data,
1210 makepath=True,
1211 sequence=True)
1212 nodeid = path.split("/")[-1]
1213 return nodeid
1214
James E. Blair6ab79e02017-01-06 10:10:17 -08001215 def addFailRequest(self, request):
1216 self.fail_requests.add(request['_oid'])
1217
James E. Blairdce6cea2016-12-20 16:45:32 -08001218 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001219 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001220 return
1221 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001222 oid = request['_oid']
1223 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001224
James E. Blair6ab79e02017-01-06 10:10:17 -08001225 if oid in self.fail_requests:
1226 request['state'] = 'failed'
1227 else:
1228 request['state'] = 'fulfilled'
1229 nodes = []
1230 for node in request['node_types']:
1231 nodeid = self.makeNode(oid, node)
1232 nodes.append(nodeid)
1233 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001234
James E. Blaira38c28e2017-01-04 10:33:20 -08001235 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001236 path = self.REQUEST_ROOT + '/' + oid
1237 data = json.dumps(request)
1238 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1239 self.client.set(path, data)
1240
1241
James E. Blair498059b2016-12-20 13:50:13 -08001242class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001243 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001244 super(ChrootedKazooFixture, self).__init__()
1245
1246 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1247 if ':' in zk_host:
1248 host, port = zk_host.split(':')
1249 else:
1250 host = zk_host
1251 port = None
1252
1253 self.zookeeper_host = host
1254
1255 if not port:
1256 self.zookeeper_port = 2181
1257 else:
1258 self.zookeeper_port = int(port)
1259
Clark Boylan621ec9a2017-04-07 17:41:33 -07001260 self.test_id = test_id
1261
James E. Blair498059b2016-12-20 13:50:13 -08001262 def _setUp(self):
1263 # Make sure the test chroot paths do not conflict
1264 random_bits = ''.join(random.choice(string.ascii_lowercase +
1265 string.ascii_uppercase)
1266 for x in range(8))
1267
Clark Boylan621ec9a2017-04-07 17:41:33 -07001268 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001269 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1270
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001271 self.addCleanup(self._cleanup)
1272
James E. Blair498059b2016-12-20 13:50:13 -08001273 # Ensure the chroot path exists and clean up any pre-existing znodes.
1274 _tmp_client = kazoo.client.KazooClient(
1275 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1276 _tmp_client.start()
1277
1278 if _tmp_client.exists(self.zookeeper_chroot):
1279 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1280
1281 _tmp_client.ensure_path(self.zookeeper_chroot)
1282 _tmp_client.stop()
1283 _tmp_client.close()
1284
James E. Blair498059b2016-12-20 13:50:13 -08001285 def _cleanup(self):
1286 '''Remove the chroot path.'''
1287 # Need a non-chroot'ed client to remove the chroot path
1288 _tmp_client = kazoo.client.KazooClient(
1289 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1290 _tmp_client.start()
1291 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1292 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001293 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001294
1295
Joshua Heskethd78b4482015-09-14 16:56:34 -06001296class MySQLSchemaFixture(fixtures.Fixture):
1297 def setUp(self):
1298 super(MySQLSchemaFixture, self).setUp()
1299
1300 random_bits = ''.join(random.choice(string.ascii_lowercase +
1301 string.ascii_uppercase)
1302 for x in range(8))
1303 self.name = '%s_%s' % (random_bits, os.getpid())
1304 self.passwd = uuid.uuid4().hex
1305 db = pymysql.connect(host="localhost",
1306 user="openstack_citest",
1307 passwd="openstack_citest",
1308 db="openstack_citest")
1309 cur = db.cursor()
1310 cur.execute("create database %s" % self.name)
1311 cur.execute(
1312 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1313 (self.name, self.name, self.passwd))
1314 cur.execute("flush privileges")
1315
1316 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1317 self.passwd,
1318 self.name)
1319 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1320 self.addCleanup(self.cleanup)
1321
1322 def cleanup(self):
1323 db = pymysql.connect(host="localhost",
1324 user="openstack_citest",
1325 passwd="openstack_citest",
1326 db="openstack_citest")
1327 cur = db.cursor()
1328 cur.execute("drop database %s" % self.name)
1329 cur.execute("drop user '%s'@'localhost'" % self.name)
1330 cur.execute("flush privileges")
1331
1332
Maru Newby3fe5f852015-01-13 04:22:14 +00001333class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001334 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001335 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001336
James E. Blair1c236df2017-02-01 14:07:24 -08001337 def attachLogs(self, *args):
1338 def reader():
1339 self._log_stream.seek(0)
1340 while True:
1341 x = self._log_stream.read(4096)
1342 if not x:
1343 break
1344 yield x.encode('utf8')
1345 content = testtools.content.content_from_reader(
1346 reader,
1347 testtools.content_type.UTF8_TEXT,
1348 False)
1349 self.addDetail('logging', content)
1350
Clark Boylanb640e052014-04-03 16:41:46 -07001351 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001352 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001353 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1354 try:
1355 test_timeout = int(test_timeout)
1356 except ValueError:
1357 # If timeout value is invalid do not set a timeout.
1358 test_timeout = 0
1359 if test_timeout > 0:
1360 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1361
1362 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1363 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1364 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1365 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1366 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1367 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1368 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1369 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1370 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1371 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001372 self._log_stream = StringIO()
1373 self.addOnException(self.attachLogs)
1374 else:
1375 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001376
James E. Blair1c236df2017-02-01 14:07:24 -08001377 handler = logging.StreamHandler(self._log_stream)
1378 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1379 '%(levelname)-8s %(message)s')
1380 handler.setFormatter(formatter)
1381
1382 logger = logging.getLogger()
1383 logger.setLevel(logging.DEBUG)
1384 logger.addHandler(handler)
1385
Clark Boylan3410d532017-04-25 12:35:29 -07001386 # Make sure we don't carry old handlers around in process state
1387 # which slows down test runs
1388 self.addCleanup(logger.removeHandler, handler)
1389 self.addCleanup(handler.close)
1390 self.addCleanup(handler.flush)
1391
James E. Blair1c236df2017-02-01 14:07:24 -08001392 # NOTE(notmorgan): Extract logging overrides for specific
1393 # libraries from the OS_LOG_DEFAULTS env and create loggers
1394 # for each. This is used to limit the output during test runs
1395 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001396 log_defaults_from_env = os.environ.get(
1397 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001398 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001399
James E. Blairdce6cea2016-12-20 16:45:32 -08001400 if log_defaults_from_env:
1401 for default in log_defaults_from_env.split(','):
1402 try:
1403 name, level_str = default.split('=', 1)
1404 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001405 logger = logging.getLogger(name)
1406 logger.setLevel(level)
1407 logger.addHandler(handler)
1408 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001409 except ValueError:
1410 # NOTE(notmorgan): Invalid format of the log default,
1411 # skip and don't try and apply a logger for the
1412 # specified module
1413 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001414
Maru Newby3fe5f852015-01-13 04:22:14 +00001415
1416class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001417 """A test case with a functioning Zuul.
1418
1419 The following class variables are used during test setup and can
1420 be overidden by subclasses but are effectively read-only once a
1421 test method starts running:
1422
1423 :cvar str config_file: This points to the main zuul config file
1424 within the fixtures directory. Subclasses may override this
1425 to obtain a different behavior.
1426
1427 :cvar str tenant_config_file: This is the tenant config file
1428 (which specifies from what git repos the configuration should
1429 be loaded). It defaults to the value specified in
1430 `config_file` but can be overidden by subclasses to obtain a
1431 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001432 configuration. See also the :py:func:`simple_layout`
1433 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001434
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001435 :cvar bool create_project_keys: Indicates whether Zuul should
1436 auto-generate keys for each project, or whether the test
1437 infrastructure should insert dummy keys to save time during
1438 startup. Defaults to False.
1439
James E. Blaire7b99a02016-08-05 14:27:34 -07001440 The following are instance variables that are useful within test
1441 methods:
1442
1443 :ivar FakeGerritConnection fake_<connection>:
1444 A :py:class:`~tests.base.FakeGerritConnection` will be
1445 instantiated for each connection present in the config file
1446 and stored here. For instance, `fake_gerrit` will hold the
1447 FakeGerritConnection object for a connection named `gerrit`.
1448
1449 :ivar FakeGearmanServer gearman_server: An instance of
1450 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1451 server that all of the Zuul components in this test use to
1452 communicate with each other.
1453
Paul Belanger174a8272017-03-14 13:20:10 -04001454 :ivar RecordingExecutorServer executor_server: An instance of
1455 :py:class:`~tests.base.RecordingExecutorServer` which is the
1456 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001457
1458 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1459 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001460 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001461 list upon completion.
1462
1463 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1464 objects representing completed builds. They are appended to
1465 the list in the order they complete.
1466
1467 """
1468
James E. Blair83005782015-12-11 14:46:03 -08001469 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001470 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001471 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001472
1473 def _startMerger(self):
1474 self.merge_server = zuul.merger.server.MergeServer(self.config,
1475 self.connections)
1476 self.merge_server.start()
1477
Maru Newby3fe5f852015-01-13 04:22:14 +00001478 def setUp(self):
1479 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001480
1481 self.setupZK()
1482
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001483 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001484 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001485 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1486 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001487 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001488 tmp_root = tempfile.mkdtemp(
1489 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001490 self.test_root = os.path.join(tmp_root, "zuul-test")
1491 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001492 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001493 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001494 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001495
1496 if os.path.exists(self.test_root):
1497 shutil.rmtree(self.test_root)
1498 os.makedirs(self.test_root)
1499 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001500 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001501
1502 # Make per test copy of Configuration.
1503 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001504 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001505 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001506 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001507 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001508 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001509 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001510
Clark Boylanb640e052014-04-03 16:41:46 -07001511 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001512 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1513 # see: https://github.com/jsocol/pystatsd/issues/61
1514 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001515 os.environ['STATSD_PORT'] = str(self.statsd.port)
1516 self.statsd.start()
1517 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001518 reload_module(statsd)
1519 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001520
1521 self.gearman_server = FakeGearmanServer()
1522
1523 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001524 self.log.info("Gearman server on port %s" %
1525 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001526
James E. Blaire511d2f2016-12-08 15:22:26 -08001527 gerritsource.GerritSource.replication_timeout = 1.5
1528 gerritsource.GerritSource.replication_retry_interval = 0.5
1529 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001530
Joshua Hesketh352264b2015-08-11 23:42:08 +10001531 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001532
Jan Hruban7083edd2015-08-21 14:00:54 +02001533 self.webapp = zuul.webapp.WebApp(
1534 self.sched, port=0, listen_address='127.0.0.1')
1535
Jan Hruban6b71aff2015-10-22 16:58:08 +02001536 self.event_queues = [
1537 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001538 self.sched.trigger_event_queue,
1539 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001540 ]
1541
James E. Blairfef78942016-03-11 16:28:56 -08001542 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001543 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001544
Clark Boylanb640e052014-04-03 16:41:46 -07001545 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001546 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001547 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001548 return FakeURLOpener(self.upstream_root, *args, **kw)
1549
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001550 old_urlopen = urllib.request.urlopen
1551 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001552
James E. Blair3f876d52016-07-22 13:07:14 -07001553 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001554
Paul Belanger174a8272017-03-14 13:20:10 -04001555 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001556 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001557 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001558 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001559 _test_root=self.test_root,
1560 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001561 self.executor_server.start()
1562 self.history = self.executor_server.build_history
1563 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001564
Paul Belanger174a8272017-03-14 13:20:10 -04001565 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001566 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001567 self.merge_client = zuul.merger.client.MergeClient(
1568 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001569 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001570 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001571 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001572
James E. Blair0d5a36e2017-02-21 10:53:44 -05001573 self.fake_nodepool = FakeNodepool(
1574 self.zk_chroot_fixture.zookeeper_host,
1575 self.zk_chroot_fixture.zookeeper_port,
1576 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001577
Paul Belanger174a8272017-03-14 13:20:10 -04001578 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001579 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001580 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001582
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001583 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001584
1585 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001586 self.webapp.start()
1587 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001588 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001589 # Cleanups are run in reverse order
1590 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001591 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001592 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001593
James E. Blairb9c0d772017-03-03 14:34:49 -08001594 self.sched.reconfigure(self.config)
1595 self.sched.resume()
1596
James E. Blairfef78942016-03-11 16:28:56 -08001597 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001598 # Set up gerrit related fakes
1599 # Set a changes database so multiple FakeGerrit's can report back to
1600 # a virtual canonical database given by the configured hostname
1601 self.gerrit_changes_dbs = {}
1602
1603 def getGerritConnection(driver, name, config):
1604 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1605 con = FakeGerritConnection(driver, name, config,
1606 changes_db=db,
1607 upstream_root=self.upstream_root)
1608 self.event_queues.append(con.event_queue)
1609 setattr(self, 'fake_' + name, con)
1610 return con
1611
1612 self.useFixture(fixtures.MonkeyPatch(
1613 'zuul.driver.gerrit.GerritDriver.getConnection',
1614 getGerritConnection))
1615
Gregory Haynes4fc12542015-04-22 20:38:06 -07001616 def getGithubConnection(driver, name, config):
1617 con = FakeGithubConnection(driver, name, config,
1618 upstream_root=self.upstream_root)
1619 setattr(self, 'fake_' + name, con)
1620 return con
1621
1622 self.useFixture(fixtures.MonkeyPatch(
1623 'zuul.driver.github.GithubDriver.getConnection',
1624 getGithubConnection))
1625
James E. Blaire511d2f2016-12-08 15:22:26 -08001626 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001627 # TODO(jhesketh): This should come from lib.connections for better
1628 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001629 # Register connections from the config
1630 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001631
Joshua Hesketh352264b2015-08-11 23:42:08 +10001632 def FakeSMTPFactory(*args, **kw):
1633 args = [self.smtp_messages] + list(args)
1634 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001635
Joshua Hesketh352264b2015-08-11 23:42:08 +10001636 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001637
James E. Blaire511d2f2016-12-08 15:22:26 -08001638 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001639 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001640 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001641
James E. Blair83005782015-12-11 14:46:03 -08001642 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001643 # This creates the per-test configuration object. It can be
1644 # overriden by subclasses, but should not need to be since it
1645 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001646 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001647 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001648
1649 if not self.setupSimpleLayout():
1650 if hasattr(self, 'tenant_config_file'):
1651 self.config.set('zuul', 'tenant_config',
1652 self.tenant_config_file)
1653 git_path = os.path.join(
1654 os.path.dirname(
1655 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1656 'git')
1657 if os.path.exists(git_path):
1658 for reponame in os.listdir(git_path):
1659 project = reponame.replace('_', '/')
1660 self.copyDirToRepo(project,
1661 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001662 self.setupAllProjectKeys()
1663
James E. Blair06cc3922017-04-19 10:08:10 -07001664 def setupSimpleLayout(self):
1665 # If the test method has been decorated with a simple_layout,
1666 # use that instead of the class tenant_config_file. Set up a
1667 # single config-project with the specified layout, and
1668 # initialize repos for all of the 'project' entries which
1669 # appear in the layout.
1670 test_name = self.id().split('.')[-1]
1671 test = getattr(self, test_name)
1672 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001673 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001674 else:
1675 return False
1676
James E. Blairb70e55a2017-04-19 12:57:02 -07001677 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001678 path = os.path.join(FIXTURE_DIR, path)
1679 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001680 data = f.read()
1681 layout = yaml.safe_load(data)
1682 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001683 untrusted_projects = []
1684 for item in layout:
1685 if 'project' in item:
1686 name = item['project']['name']
1687 untrusted_projects.append(name)
1688 self.init_repo(name)
1689 self.addCommitToRepo(name, 'initial commit',
1690 files={'README': ''},
1691 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001692 if 'job' in item:
1693 jobname = item['job']['name']
1694 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001695
1696 root = os.path.join(self.test_root, "config")
1697 if not os.path.exists(root):
1698 os.makedirs(root)
1699 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1700 config = [{'tenant':
1701 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001702 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001703 {'config-projects': ['common-config'],
1704 'untrusted-projects': untrusted_projects}}}}]
1705 f.write(yaml.dump(config))
1706 f.close()
1707 self.config.set('zuul', 'tenant_config',
1708 os.path.join(FIXTURE_DIR, f.name))
1709
1710 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001711 self.addCommitToRepo('common-config', 'add content from fixture',
1712 files, branch='master', tag='init')
1713
1714 return True
1715
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001716 def setupAllProjectKeys(self):
1717 if self.create_project_keys:
1718 return
1719
1720 path = self.config.get('zuul', 'tenant_config')
1721 with open(os.path.join(FIXTURE_DIR, path)) as f:
1722 tenant_config = yaml.safe_load(f.read())
1723 for tenant in tenant_config:
1724 sources = tenant['tenant']['source']
1725 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001726 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001727 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001728 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001729 self.setupProjectKeys(source, project)
1730
1731 def setupProjectKeys(self, source, project):
1732 # Make sure we set up an RSA key for the project so that we
1733 # don't spend time generating one:
1734
1735 key_root = os.path.join(self.state_root, 'keys')
1736 if not os.path.isdir(key_root):
1737 os.mkdir(key_root, 0o700)
1738 private_key_file = os.path.join(key_root, source, project + '.pem')
1739 private_key_dir = os.path.dirname(private_key_file)
1740 self.log.debug("Installing test keys for project %s at %s" % (
1741 project, private_key_file))
1742 if not os.path.isdir(private_key_dir):
1743 os.makedirs(private_key_dir)
1744 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1745 with open(private_key_file, 'w') as o:
1746 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001747
James E. Blair498059b2016-12-20 13:50:13 -08001748 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001749 self.zk_chroot_fixture = self.useFixture(
1750 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001751 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001752 self.zk_chroot_fixture.zookeeper_host,
1753 self.zk_chroot_fixture.zookeeper_port,
1754 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001755
James E. Blair96c6bf82016-01-15 16:20:40 -08001756 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001757 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001758
1759 files = {}
1760 for (dirpath, dirnames, filenames) in os.walk(source_path):
1761 for filename in filenames:
1762 test_tree_filepath = os.path.join(dirpath, filename)
1763 common_path = os.path.commonprefix([test_tree_filepath,
1764 source_path])
1765 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1766 with open(test_tree_filepath, 'r') as f:
1767 content = f.read()
1768 files[relative_filepath] = content
1769 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001770 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001771
James E. Blaire18d4602017-01-05 11:17:28 -08001772 def assertNodepoolState(self):
1773 # Make sure that there are no pending requests
1774
1775 requests = self.fake_nodepool.getNodeRequests()
1776 self.assertEqual(len(requests), 0)
1777
1778 nodes = self.fake_nodepool.getNodes()
1779 for node in nodes:
1780 self.assertFalse(node['_lock'], "Node %s is locked" %
1781 (node['_oid'],))
1782
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001783 def assertNoGeneratedKeys(self):
1784 # Make sure that Zuul did not generate any project keys
1785 # (unless it was supposed to).
1786
1787 if self.create_project_keys:
1788 return
1789
1790 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1791 test_key = i.read()
1792
1793 key_root = os.path.join(self.state_root, 'keys')
1794 for root, dirname, files in os.walk(key_root):
1795 for fn in files:
1796 with open(os.path.join(root, fn)) as f:
1797 self.assertEqual(test_key, f.read())
1798
Clark Boylanb640e052014-04-03 16:41:46 -07001799 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001800 self.log.debug("Assert final state")
1801 # Make sure no jobs are running
1802 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001803 # Make sure that git.Repo objects have been garbage collected.
1804 repos = []
1805 gc.collect()
1806 for obj in gc.get_objects():
1807 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001808 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001809 repos.append(obj)
1810 self.assertEqual(len(repos), 0)
1811 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001812 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001813 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001814 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001815 for tenant in self.sched.abide.tenants.values():
1816 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001817 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001818 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001819
1820 def shutdown(self):
1821 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001822 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001823 self.merge_server.stop()
1824 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001825 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001826 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001827 self.sched.stop()
1828 self.sched.join()
1829 self.statsd.stop()
1830 self.statsd.join()
1831 self.webapp.stop()
1832 self.webapp.join()
1833 self.rpc.stop()
1834 self.rpc.join()
1835 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001836 self.fake_nodepool.stop()
1837 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001838 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001839 # we whitelist watchdog threads as they have relatively long delays
1840 # before noticing they should exit, but they should exit on their own.
1841 threads = [t for t in threading.enumerate()
1842 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001843 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001844 log_str = ""
1845 for thread_id, stack_frame in sys._current_frames().items():
1846 log_str += "Thread: %s\n" % thread_id
1847 log_str += "".join(traceback.format_stack(stack_frame))
1848 self.log.debug(log_str)
1849 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07001850
James E. Blaira002b032017-04-18 10:35:48 -07001851 def assertCleanShutdown(self):
1852 pass
1853
James E. Blairc4ba97a2017-04-19 16:26:24 -07001854 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07001855 parts = project.split('/')
1856 path = os.path.join(self.upstream_root, *parts[:-1])
1857 if not os.path.exists(path):
1858 os.makedirs(path)
1859 path = os.path.join(self.upstream_root, project)
1860 repo = git.Repo.init(path)
1861
Morgan Fainberg78c301a2016-07-14 13:47:01 -07001862 with repo.config_writer() as config_writer:
1863 config_writer.set_value('user', 'email', 'user@example.com')
1864 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07001865
Clark Boylanb640e052014-04-03 16:41:46 -07001866 repo.index.commit('initial commit')
1867 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07001868 if tag:
1869 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07001870
James E. Blair97d902e2014-08-21 13:25:56 -07001871 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07001872 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07001873 repo.git.clean('-x', '-f', '-d')
1874
James E. Blair97d902e2014-08-21 13:25:56 -07001875 def create_branch(self, project, branch):
1876 path = os.path.join(self.upstream_root, project)
1877 repo = git.Repo.init(path)
1878 fn = os.path.join(path, 'README')
1879
1880 branch_head = repo.create_head(branch)
1881 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07001882 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07001883 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001884 f.close()
1885 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07001886 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07001887
James E. Blair97d902e2014-08-21 13:25:56 -07001888 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07001889 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07001890 repo.git.clean('-x', '-f', '-d')
1891
Sachi King9f16d522016-03-16 12:20:45 +11001892 def create_commit(self, project):
1893 path = os.path.join(self.upstream_root, project)
1894 repo = git.Repo(path)
1895 repo.head.reference = repo.heads['master']
1896 file_name = os.path.join(path, 'README')
1897 with open(file_name, 'a') as f:
1898 f.write('creating fake commit\n')
1899 repo.index.add([file_name])
1900 commit = repo.index.commit('Creating a fake commit')
1901 return commit.hexsha
1902
James E. Blairf4a5f022017-04-18 14:01:10 -07001903 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07001904 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07001905 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07001906 while len(self.builds):
1907 self.release(self.builds[0])
1908 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07001909 i += 1
1910 if count is not None and i >= count:
1911 break
James E. Blairb8c16472015-05-05 14:55:26 -07001912
Clark Boylanb640e052014-04-03 16:41:46 -07001913 def release(self, job):
1914 if isinstance(job, FakeBuild):
1915 job.release()
1916 else:
1917 job.waiting = False
1918 self.log.debug("Queued job %s released" % job.unique)
1919 self.gearman_server.wakeConnections()
1920
1921 def getParameter(self, job, name):
1922 if isinstance(job, FakeBuild):
1923 return job.parameters[name]
1924 else:
1925 parameters = json.loads(job.arguments)
1926 return parameters[name]
1927
Clark Boylanb640e052014-04-03 16:41:46 -07001928 def haveAllBuildsReported(self):
1929 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04001930 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07001931 return False
1932 # Find out if every build that the worker has completed has been
1933 # reported back to Zuul. If it hasn't then that means a Gearman
1934 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07001935 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04001936 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001937 if not zbuild:
1938 # It has already been reported
1939 continue
1940 # It hasn't been reported yet.
1941 return False
1942 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04001943 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001944 if connection.state == 'GRAB_WAIT':
1945 return False
1946 return True
1947
1948 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001949 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07001950 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07001951 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07001952 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07001953 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04001954 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07001955 for j in conn.related_jobs.values():
1956 if j.unique == build.uuid:
1957 client_job = j
1958 break
1959 if not client_job:
1960 self.log.debug("%s is not known to the gearman client" %
1961 build)
James E. Blairf15139b2015-04-02 16:37:15 -07001962 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001963 if not client_job.handle:
1964 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001965 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001966 server_job = self.gearman_server.jobs.get(client_job.handle)
1967 if not server_job:
1968 self.log.debug("%s is not known to the gearman server" %
1969 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001970 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001971 if not hasattr(server_job, 'waiting'):
1972 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001973 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001974 if server_job.waiting:
1975 continue
James E. Blair17302972016-08-10 16:11:42 -07001976 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08001977 self.log.debug("%s has not reported start" % build)
1978 return False
Paul Belanger174a8272017-03-14 13:20:10 -04001979 worker_build = self.executor_server.job_builds.get(
1980 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07001981 if worker_build:
1982 if worker_build.isWaiting():
1983 continue
1984 else:
1985 self.log.debug("%s is running" % worker_build)
1986 return False
Clark Boylanb640e052014-04-03 16:41:46 -07001987 else:
James E. Blair962220f2016-08-03 11:22:38 -07001988 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07001989 return False
James E. Blaira002b032017-04-18 10:35:48 -07001990 for (build_uuid, job_worker) in \
1991 self.executor_server.job_workers.items():
1992 if build_uuid not in seen_builds:
1993 self.log.debug("%s is not finalized" % build_uuid)
1994 return False
James E. Blairf15139b2015-04-02 16:37:15 -07001995 return True
Clark Boylanb640e052014-04-03 16:41:46 -07001996
James E. Blairdce6cea2016-12-20 16:45:32 -08001997 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001998 if self.fake_nodepool.paused:
1999 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002000 if self.sched.nodepool.requests:
2001 return False
2002 return True
2003
Jan Hruban6b71aff2015-10-22 16:58:08 +02002004 def eventQueuesEmpty(self):
2005 for queue in self.event_queues:
2006 yield queue.empty()
2007
2008 def eventQueuesJoin(self):
2009 for queue in self.event_queues:
2010 queue.join()
2011
Clark Boylanb640e052014-04-03 16:41:46 -07002012 def waitUntilSettled(self):
2013 self.log.debug("Waiting until settled...")
2014 start = time.time()
2015 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002016 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002017 self.log.error("Timeout waiting for Zuul to settle")
2018 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002019 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002020 self.log.error(" %s: %s" % (queue, queue.empty()))
2021 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002022 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002023 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002024 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002025 self.log.error("All requests completed: %s" %
2026 (self.areAllNodeRequestsComplete(),))
2027 self.log.error("Merge client jobs: %s" %
2028 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002029 raise Exception("Timeout waiting for Zuul to settle")
2030 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002031
Paul Belanger174a8272017-03-14 13:20:10 -04002032 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002033 # have all build states propogated to zuul?
2034 if self.haveAllBuildsReported():
2035 # Join ensures that the queue is empty _and_ events have been
2036 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002037 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002038 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002039 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002040 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002041 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002042 self.areAllNodeRequestsComplete() and
2043 all(self.eventQueuesEmpty())):
2044 # The queue empty check is placed at the end to
2045 # ensure that if a component adds an event between
2046 # when locked the run handler and checked that the
2047 # components were stable, we don't erroneously
2048 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002049 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.log.debug("...settled.")
2052 return
2053 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002054 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002055 self.sched.wake_event.wait(0.1)
2056
2057 def countJobResults(self, jobs, result):
2058 jobs = filter(lambda x: x.result == result, jobs)
2059 return len(jobs)
2060
James E. Blair96c6bf82016-01-15 16:20:40 -08002061 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002062 for job in self.history:
2063 if (job.name == name and
2064 (project is None or
2065 job.parameters['ZUUL_PROJECT'] == project)):
2066 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002067 raise Exception("Unable to find job %s in history" % name)
2068
2069 def assertEmptyQueues(self):
2070 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002071 for tenant in self.sched.abide.tenants.values():
2072 for pipeline in tenant.layout.pipelines.values():
2073 for queue in pipeline.queues:
2074 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002075 print('pipeline %s queue %s contents %s' % (
2076 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002077 self.assertEqual(len(queue.queue), 0,
2078 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002079
2080 def assertReportedStat(self, key, value=None, kind=None):
2081 start = time.time()
2082 while time.time() < (start + 5):
2083 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002084 k, v = stat.split(':')
2085 if key == k:
2086 if value is None and kind is None:
2087 return
2088 elif value:
2089 if value == v:
2090 return
2091 elif kind:
2092 if v.endswith('|' + kind):
2093 return
2094 time.sleep(0.1)
2095
Clark Boylanb640e052014-04-03 16:41:46 -07002096 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002097
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002098 def assertBuilds(self, builds):
2099 """Assert that the running builds are as described.
2100
2101 The list of running builds is examined and must match exactly
2102 the list of builds described by the input.
2103
2104 :arg list builds: A list of dictionaries. Each item in the
2105 list must match the corresponding build in the build
2106 history, and each element of the dictionary must match the
2107 corresponding attribute of the build.
2108
2109 """
James E. Blair3158e282016-08-19 09:34:11 -07002110 try:
2111 self.assertEqual(len(self.builds), len(builds))
2112 for i, d in enumerate(builds):
2113 for k, v in d.items():
2114 self.assertEqual(
2115 getattr(self.builds[i], k), v,
2116 "Element %i in builds does not match" % (i,))
2117 except Exception:
2118 for build in self.builds:
2119 self.log.error("Running build: %s" % build)
2120 else:
2121 self.log.error("No running builds")
2122 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002123
James E. Blairb536ecc2016-08-31 10:11:42 -07002124 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002125 """Assert that the completed builds are as described.
2126
2127 The list of completed builds is examined and must match
2128 exactly the list of builds described by the input.
2129
2130 :arg list history: A list of dictionaries. Each item in the
2131 list must match the corresponding build in the build
2132 history, and each element of the dictionary must match the
2133 corresponding attribute of the build.
2134
James E. Blairb536ecc2016-08-31 10:11:42 -07002135 :arg bool ordered: If true, the history must match the order
2136 supplied, if false, the builds are permitted to have
2137 arrived in any order.
2138
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002139 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002140 def matches(history_item, item):
2141 for k, v in item.items():
2142 if getattr(history_item, k) != v:
2143 return False
2144 return True
James E. Blair3158e282016-08-19 09:34:11 -07002145 try:
2146 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002147 if ordered:
2148 for i, d in enumerate(history):
2149 if not matches(self.history[i], d):
2150 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002151 "Element %i in history does not match %s" %
2152 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002153 else:
2154 unseen = self.history[:]
2155 for i, d in enumerate(history):
2156 found = False
2157 for unseen_item in unseen:
2158 if matches(unseen_item, d):
2159 found = True
2160 unseen.remove(unseen_item)
2161 break
2162 if not found:
2163 raise Exception("No match found for element %i "
2164 "in history" % (i,))
2165 if unseen:
2166 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002167 except Exception:
2168 for build in self.history:
2169 self.log.error("Completed build: %s" % build)
2170 else:
2171 self.log.error("No completed builds")
2172 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002173
James E. Blair6ac368c2016-12-22 18:07:20 -08002174 def printHistory(self):
2175 """Log the build history.
2176
2177 This can be useful during tests to summarize what jobs have
2178 completed.
2179
2180 """
2181 self.log.debug("Build history:")
2182 for build in self.history:
2183 self.log.debug(build)
2184
James E. Blair59fdbac2015-12-07 17:08:06 -08002185 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002186 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2187
James E. Blair9ea70072017-04-19 16:05:30 -07002188 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002189 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002190 if not os.path.exists(root):
2191 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002192 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2193 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002194- tenant:
2195 name: openstack
2196 source:
2197 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002198 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002199 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002200 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002201 - org/project
2202 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002203 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002204 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002205 self.config.set('zuul', 'tenant_config',
2206 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002207 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002208
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002209 def addCommitToRepo(self, project, message, files,
2210 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002211 path = os.path.join(self.upstream_root, project)
2212 repo = git.Repo(path)
2213 repo.head.reference = branch
2214 zuul.merger.merger.reset_repo_to_head(repo)
2215 for fn, content in files.items():
2216 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002217 try:
2218 os.makedirs(os.path.dirname(fn))
2219 except OSError:
2220 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002221 with open(fn, 'w') as f:
2222 f.write(content)
2223 repo.index.add([fn])
2224 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002225 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002226 repo.heads[branch].commit = commit
2227 repo.head.reference = branch
2228 repo.git.clean('-x', '-f', '-d')
2229 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002230 if tag:
2231 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002232 return before
2233
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002234 def commitConfigUpdate(self, project_name, source_name):
2235 """Commit an update to zuul.yaml
2236
2237 This overwrites the zuul.yaml in the specificed project with
2238 the contents specified.
2239
2240 :arg str project_name: The name of the project containing
2241 zuul.yaml (e.g., common-config)
2242
2243 :arg str source_name: The path to the file (underneath the
2244 test fixture directory) whose contents should be used to
2245 replace zuul.yaml.
2246 """
2247
2248 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002249 files = {}
2250 with open(source_path, 'r') as f:
2251 data = f.read()
2252 layout = yaml.safe_load(data)
2253 files['zuul.yaml'] = data
2254 for item in layout:
2255 if 'job' in item:
2256 jobname = item['job']['name']
2257 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002258 before = self.addCommitToRepo(
2259 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002260 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002261 return before
2262
James E. Blair7fc8daa2016-08-08 15:37:15 -07002263 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002264
James E. Blair7fc8daa2016-08-08 15:37:15 -07002265 """Inject a Fake (Gerrit) event.
2266
2267 This method accepts a JSON-encoded event and simulates Zuul
2268 having received it from Gerrit. It could (and should)
2269 eventually apply to any connection type, but is currently only
2270 used with Gerrit connections. The name of the connection is
2271 used to look up the corresponding server, and the event is
2272 simulated as having been received by all Zuul connections
2273 attached to that server. So if two Gerrit connections in Zuul
2274 are connected to the same Gerrit server, and you invoke this
2275 method specifying the name of one of them, the event will be
2276 received by both.
2277
2278 .. note::
2279
2280 "self.fake_gerrit.addEvent" calls should be migrated to
2281 this method.
2282
2283 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002284 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002285 :arg str event: The JSON-encoded event.
2286
2287 """
2288 specified_conn = self.connections.connections[connection]
2289 for conn in self.connections.connections.values():
2290 if (isinstance(conn, specified_conn.__class__) and
2291 specified_conn.server == conn.server):
2292 conn.addEvent(event)
2293
James E. Blair3f876d52016-07-22 13:07:14 -07002294
2295class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002296 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002297 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002298
Joshua Heskethd78b4482015-09-14 16:56:34 -06002299
2300class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002301 def setup_config(self):
2302 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002303 for section_name in self.config.sections():
2304 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2305 section_name, re.I)
2306 if not con_match:
2307 continue
2308
2309 if self.config.get(section_name, 'driver') == 'sql':
2310 f = MySQLSchemaFixture()
2311 self.useFixture(f)
2312 if (self.config.get(section_name, 'dburi') ==
2313 '$MYSQL_FIXTURE_DBURI$'):
2314 self.config.set(section_name, 'dburi', f.dburi)