blob: 382a5936f9cd9c501e4048d6e702bf65a133b61e [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
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
93 return hashlib.sha1(str(random.random())).hexdigest()
94
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
James E. Blair8b5408c2016-08-08 15:37:46 -0700499 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500 if 'label' in action:
501 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000505
Clark Boylanb640e052014-04-03 16:41:46 -0700506 if 'submit' in action:
507 change.setMerged()
508 if message:
509 change.setReported()
510
511 def query(self, number):
512 change = self.changes.get(int(number))
513 if change:
514 return change.query()
515 return {}
516
James E. Blairc494d542014-08-06 09:23:52 -0700517 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700518 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700519 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800520 if query.startswith('change:'):
521 # Query a specific changeid
522 changeid = query[len('change:'):]
523 l = [change.query() for change in self.changes.values()
524 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700525 elif query.startswith('message:'):
526 # Query the content of a commit message
527 msg = query[len('message:'):].strip()
528 l = [change.query() for change in self.changes.values()
529 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800530 else:
531 # Query all open changes
532 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700533 return l
James E. Blairc494d542014-08-06 09:23:52 -0700534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 pass
537
Joshua Hesketh352264b2015-08-11 23:42:08 +1000538 def getGitUrl(self, project):
539 return os.path.join(self.upstream_root, project.name)
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541
Gregory Haynes4fc12542015-04-22 20:38:06 -0700542class GithubChangeReference(git.Reference):
543 _common_path_default = "refs/pull"
544 _points_to_commits_only = True
545
546
547class FakeGithubPullRequest(object):
548
549 def __init__(self, github, number, project, branch,
550 upstream_root, number_of_commits=1):
551 """Creates a new PR with several commits.
552 Sends an event about opened PR."""
553 self.github = github
554 self.source = github
555 self.number = number
556 self.project = project
557 self.branch = branch
558 self.upstream_root = upstream_root
559 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100560 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100561 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700562 self.updated_at = None
563 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100564 self.is_merged = False
Gregory Haynes4fc12542015-04-22 20:38:06 -0700565 self._createPRRef()
566 self._addCommitToRepo()
567 self._updateTimeStamp()
568
569 def addCommit(self):
570 """Adds a commit on top of the actual PR head."""
571 self._addCommitToRepo()
572 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100573 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574
575 def forcePush(self):
576 """Clears actual commits and add a commit on top of the base."""
577 self._addCommitToRepo(reset=True)
578 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100579 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580
581 def getPullRequestOpenedEvent(self):
582 return self._getPullRequestEvent('opened')
583
584 def getPullRequestSynchronizeEvent(self):
585 return self._getPullRequestEvent('synchronize')
586
587 def getPullRequestReopenedEvent(self):
588 return self._getPullRequestEvent('reopened')
589
590 def getPullRequestClosedEvent(self):
591 return self._getPullRequestEvent('closed')
592
593 def addComment(self, message):
594 self.comments.append(message)
595 self._updateTimeStamp()
596
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200597 def getCommentAddedEvent(self, text):
598 name = 'issue_comment'
599 data = {
600 'action': 'created',
601 'issue': {
602 'number': self.number
603 },
604 'comment': {
605 'body': text
606 },
607 'repository': {
608 'full_name': self.project
609 }
610 }
611 return (name, data)
612
Jan Hruban16ad31f2015-11-07 14:39:07 +0100613 def addLabel(self, name):
614 if name not in self.labels:
615 self.labels.append(name)
616 self._updateTimeStamp()
617 return self._getLabelEvent(name)
618
619 def removeLabel(self, name):
620 if name in self.labels:
621 self.labels.remove(name)
622 self._updateTimeStamp()
623 return self._getUnlabelEvent(name)
624
625 def _getLabelEvent(self, label):
626 name = 'pull_request'
627 data = {
628 'action': 'labeled',
629 'pull_request': {
630 'number': self.number,
631 'updated_at': self.updated_at,
632 'base': {
633 'ref': self.branch,
634 'repo': {
635 'full_name': self.project
636 }
637 },
638 'head': {
639 'sha': self.head_sha
640 }
641 },
642 'label': {
643 'name': label
644 }
645 }
646 return (name, data)
647
648 def _getUnlabelEvent(self, label):
649 name = 'pull_request'
650 data = {
651 'action': 'unlabeled',
652 'pull_request': {
653 'number': self.number,
654 'updated_at': self.updated_at,
655 'base': {
656 'ref': self.branch,
657 'repo': {
658 'full_name': self.project
659 }
660 },
661 'head': {
662 'sha': self.head_sha
663 }
664 },
665 'label': {
666 'name': label
667 }
668 }
669 return (name, data)
670
Gregory Haynes4fc12542015-04-22 20:38:06 -0700671 def _getRepo(self):
672 repo_path = os.path.join(self.upstream_root, self.project)
673 return git.Repo(repo_path)
674
675 def _createPRRef(self):
676 repo = self._getRepo()
677 GithubChangeReference.create(
678 repo, self._getPRReference(), 'refs/tags/init')
679
680 def _addCommitToRepo(self, reset=False):
681 repo = self._getRepo()
682 ref = repo.references[self._getPRReference()]
683 if reset:
684 ref.set_object('refs/tags/init')
685 repo.head.reference = ref
686 zuul.merger.merger.reset_repo_to_head(repo)
687 repo.git.clean('-x', '-f', '-d')
688
689 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
690 msg = 'test-%s' % self.number
691 fn = os.path.join(repo.working_dir, fn)
692 f = open(fn, 'w')
693 with open(fn, 'w') as f:
694 f.write("test %s %s\n" %
695 (self.branch, self.number))
696 repo.index.add([fn])
697
698 self.head_sha = repo.index.commit(msg).hexsha
699 repo.head.reference = 'master'
700 zuul.merger.merger.reset_repo_to_head(repo)
701 repo.git.clean('-x', '-f', '-d')
702 repo.heads['master'].checkout()
703
704 def _updateTimeStamp(self):
705 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
706
707 def getPRHeadSha(self):
708 repo = self._getRepo()
709 return repo.references[self._getPRReference()].commit.hexsha
710
Jan Hrubane252a732017-01-03 15:03:09 +0100711 def setStatus(self, state, url, description, context):
712 self.statuses[context] = {
713 'state': state,
714 'url': url,
715 'description': description
716 }
717
718 def _clearStatuses(self):
719 self.statuses = {}
720
Gregory Haynes4fc12542015-04-22 20:38:06 -0700721 def _getPRReference(self):
722 return '%s/head' % self.number
723
724 def _getPullRequestEvent(self, action):
725 name = 'pull_request'
726 data = {
727 'action': action,
728 'number': self.number,
729 'pull_request': {
730 'number': self.number,
731 'updated_at': self.updated_at,
732 'base': {
733 'ref': self.branch,
734 'repo': {
735 'full_name': self.project
736 }
737 },
738 'head': {
739 'sha': self.head_sha
740 }
741 }
742 }
743 return (name, data)
744
745
746class FakeGithubConnection(githubconnection.GithubConnection):
747 log = logging.getLogger("zuul.test.FakeGithubConnection")
748
749 def __init__(self, driver, connection_name, connection_config,
750 upstream_root=None):
751 super(FakeGithubConnection, self).__init__(driver, connection_name,
752 connection_config)
753 self.connection_name = connection_name
754 self.pr_number = 0
755 self.pull_requests = []
756 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100757 self.merge_failure = False
758 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700759
760 def openFakePullRequest(self, project, branch):
761 self.pr_number += 1
762 pull_request = FakeGithubPullRequest(
763 self, self.pr_number, project, branch, self.upstream_root)
764 self.pull_requests.append(pull_request)
765 return pull_request
766
Wayne1a78c612015-06-11 17:14:13 -0700767 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
768 if not old_rev:
769 old_rev = '00000000000000000000000000000000'
770 if not new_rev:
771 new_rev = random_sha1()
772 name = 'push'
773 data = {
774 'ref': ref,
775 'before': old_rev,
776 'after': new_rev,
777 'repository': {
778 'full_name': project
779 }
780 }
781 return (name, data)
782
Gregory Haynes4fc12542015-04-22 20:38:06 -0700783 def emitEvent(self, event):
784 """Emulates sending the GitHub webhook event to the connection."""
785 port = self.webapp.server.socket.getsockname()[1]
786 name, data = event
787 payload = json.dumps(data)
788 headers = {'X-Github-Event': name}
789 req = urllib.request.Request(
790 'http://localhost:%s/connection/%s/payload'
791 % (port, self.connection_name),
792 data=payload, headers=headers)
793 urllib.request.urlopen(req)
794
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200795 def getPull(self, project, number):
796 pr = self.pull_requests[number - 1]
797 data = {
798 'number': number,
799 'updated_at': pr.updated_at,
800 'base': {
801 'repo': {
802 'full_name': pr.project
803 },
804 'ref': pr.branch,
805 },
806 'head': {
807 'sha': pr.head_sha
808 }
809 }
810 return data
811
Gregory Haynes4fc12542015-04-22 20:38:06 -0700812 def getGitUrl(self, project):
813 return os.path.join(self.upstream_root, str(project))
814
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200815 def real_getGitUrl(self, project):
816 return super(FakeGithubConnection, self).getGitUrl(project)
817
Gregory Haynes4fc12542015-04-22 20:38:06 -0700818 def getProjectBranches(self, project):
819 """Masks getProjectBranches since we don't have a real github"""
820
821 # just returns master for now
822 return ['master']
823
Jan Hrubane252a732017-01-03 15:03:09 +0100824 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700825 pull_request = self.pull_requests[pr_number - 1]
826 pull_request.addComment(message)
827
Jan Hruban49bff072015-11-03 11:45:46 +0100828 def mergePull(self, project, pr_number, sha=None):
829 pull_request = self.pull_requests[pr_number - 1]
830 if self.merge_failure:
831 raise Exception('Pull request was not merged')
832 if self.merge_not_allowed_count > 0:
833 self.merge_not_allowed_count -= 1
834 raise MergeFailure('Merge was not successful due to mergeability'
835 ' conflict')
836 pull_request.is_merged = True
837
Jan Hrubane252a732017-01-03 15:03:09 +0100838 def setCommitStatus(self, project, sha, state,
839 url='', description='', context=''):
840 owner, proj = project.split('/')
841 for pr in self.pull_requests:
842 pr_owner, pr_project = pr.project.split('/')
843 if (pr_owner == owner and pr_project == proj and
844 pr.head_sha == sha):
845 pr.setStatus(state, url, description, context)
846
Jan Hruban16ad31f2015-11-07 14:39:07 +0100847 def labelPull(self, project, pr_number, label):
848 pull_request = self.pull_requests[pr_number - 1]
849 pull_request.addLabel(label)
850
851 def unlabelPull(self, project, pr_number, label):
852 pull_request = self.pull_requests[pr_number - 1]
853 pull_request.removeLabel(label)
854
Gregory Haynes4fc12542015-04-22 20:38:06 -0700855
Clark Boylanb640e052014-04-03 16:41:46 -0700856class BuildHistory(object):
857 def __init__(self, **kw):
858 self.__dict__.update(kw)
859
860 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700861 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
862 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700863
864
865class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200866 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700867 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700868 self.url = url
869
870 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700871 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700872 path = res.path
873 project = '/'.join(path.split('/')[2:-2])
874 ret = '001e# service=git-upload-pack\n'
875 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
876 'multi_ack thin-pack side-band side-band-64k ofs-delta '
877 'shallow no-progress include-tag multi_ack_detailed no-done\n')
878 path = os.path.join(self.upstream_root, project)
879 repo = git.Repo(path)
880 for ref in repo.refs:
881 r = ref.object.hexsha + ' ' + ref.path + '\n'
882 ret += '%04x%s' % (len(r) + 4, r)
883 ret += '0000'
884 return ret
885
886
Clark Boylanb640e052014-04-03 16:41:46 -0700887class FakeStatsd(threading.Thread):
888 def __init__(self):
889 threading.Thread.__init__(self)
890 self.daemon = True
891 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
892 self.sock.bind(('', 0))
893 self.port = self.sock.getsockname()[1]
894 self.wake_read, self.wake_write = os.pipe()
895 self.stats = []
896
897 def run(self):
898 while True:
899 poll = select.poll()
900 poll.register(self.sock, select.POLLIN)
901 poll.register(self.wake_read, select.POLLIN)
902 ret = poll.poll()
903 for (fd, event) in ret:
904 if fd == self.sock.fileno():
905 data = self.sock.recvfrom(1024)
906 if not data:
907 return
908 self.stats.append(data[0])
909 if fd == self.wake_read:
910 return
911
912 def stop(self):
913 os.write(self.wake_write, '1\n')
914
915
James E. Blaire1767bc2016-08-02 10:00:27 -0700916class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700917 log = logging.getLogger("zuul.test")
918
Paul Belanger174a8272017-03-14 13:20:10 -0400919 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700920 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400921 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700922 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700923 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700924 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700925 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700926 # TODOv3(jeblair): self.node is really "the image of the node
927 # assigned". We should rename it (self.node_image?) if we
928 # keep using it like this, or we may end up exposing more of
929 # the complexity around multi-node jobs here
930 # (self.nodes[0].image?)
931 self.node = None
932 if len(self.parameters.get('nodes')) == 1:
933 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700934 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100935 self.pipeline = self.parameters['ZUUL_PIPELINE']
936 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700937 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700938 self.wait_condition = threading.Condition()
939 self.waiting = False
940 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500941 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700942 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700943 self.changes = None
944 if 'ZUUL_CHANGE_IDS' in self.parameters:
945 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700946
James E. Blair3158e282016-08-19 09:34:11 -0700947 def __repr__(self):
948 waiting = ''
949 if self.waiting:
950 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100951 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
952 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700953
Clark Boylanb640e052014-04-03 16:41:46 -0700954 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700955 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700956 self.wait_condition.acquire()
957 self.wait_condition.notify()
958 self.waiting = False
959 self.log.debug("Build %s released" % self.unique)
960 self.wait_condition.release()
961
962 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700963 """Return whether this build is being held.
964
965 :returns: Whether the build is being held.
966 :rtype: bool
967 """
968
Clark Boylanb640e052014-04-03 16:41:46 -0700969 self.wait_condition.acquire()
970 if self.waiting:
971 ret = True
972 else:
973 ret = False
974 self.wait_condition.release()
975 return ret
976
977 def _wait(self):
978 self.wait_condition.acquire()
979 self.waiting = True
980 self.log.debug("Build %s waiting" % self.unique)
981 self.wait_condition.wait()
982 self.wait_condition.release()
983
984 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700985 self.log.debug('Running build %s' % self.unique)
986
Paul Belanger174a8272017-03-14 13:20:10 -0400987 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700988 self.log.debug('Holding build %s' % self.unique)
989 self._wait()
990 self.log.debug("Build %s continuing" % self.unique)
991
James E. Blair412fba82017-01-26 15:00:50 -0800992 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700993 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800994 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -0700995 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -0800996 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -0500997 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -0800998 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -0700999
James E. Blaire1767bc2016-08-02 10:00:27 -07001000 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001001
James E. Blaira5dba232016-08-08 15:53:24 -07001002 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001003 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001004 for change in changes:
1005 if self.hasChanges(change):
1006 return True
1007 return False
1008
James E. Blaire7b99a02016-08-05 14:27:34 -07001009 def hasChanges(self, *changes):
1010 """Return whether this build has certain changes in its git repos.
1011
1012 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001013 are expected to be present (in order) in the git repository of
1014 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001015
1016 :returns: Whether the build has the indicated changes.
1017 :rtype: bool
1018
1019 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001020 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001021 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001022 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001023 try:
1024 repo = git.Repo(path)
1025 except NoSuchPathError as e:
1026 self.log.debug('%s' % e)
1027 return False
1028 ref = self.parameters['ZUUL_REF']
1029 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1030 commit_message = '%s-1' % change.subject
1031 self.log.debug("Checking if build %s has changes; commit_message "
1032 "%s; repo_messages %s" % (self, commit_message,
1033 repo_messages))
1034 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001035 self.log.debug(" messages do not match")
1036 return False
1037 self.log.debug(" OK")
1038 return True
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040
Paul Belanger174a8272017-03-14 13:20:10 -04001041class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1042 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001043
Paul Belanger174a8272017-03-14 13:20:10 -04001044 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001045 they will report that they have started but then pause until
1046 released before reporting completion. This attribute may be
1047 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001048 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001049 be explicitly released.
1050
1051 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001052 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001053 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001054 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001055 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001056 self.hold_jobs_in_build = False
1057 self.lock = threading.Lock()
1058 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001059 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001060 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001061 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001062
James E. Blaira5dba232016-08-08 15:53:24 -07001063 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001064 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001065
1066 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001067 :arg Change change: The :py:class:`~tests.base.FakeChange`
1068 instance which should cause the job to fail. This job
1069 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001070
1071 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001072 l = self.fail_tests.get(name, [])
1073 l.append(change)
1074 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001075
James E. Blair962220f2016-08-03 11:22:38 -07001076 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001077 """Release a held build.
1078
1079 :arg str regex: A regular expression which, if supplied, will
1080 cause only builds with matching names to be released. If
1081 not supplied, all builds will be released.
1082
1083 """
James E. Blair962220f2016-08-03 11:22:38 -07001084 builds = self.running_builds[:]
1085 self.log.debug("Releasing build %s (%s)" % (regex,
1086 len(self.running_builds)))
1087 for build in builds:
1088 if not regex or re.match(regex, build.name):
1089 self.log.debug("Releasing build %s" %
1090 (build.parameters['ZUUL_UUID']))
1091 build.release()
1092 else:
1093 self.log.debug("Not releasing build %s" %
1094 (build.parameters['ZUUL_UUID']))
1095 self.log.debug("Done releasing builds %s (%s)" %
1096 (regex, len(self.running_builds)))
1097
Paul Belanger174a8272017-03-14 13:20:10 -04001098 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001099 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001100 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001101 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001102 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001103 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001104 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001105 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001106 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1107 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001108
1109 def stopJob(self, job):
1110 self.log.debug("handle stop")
1111 parameters = json.loads(job.arguments)
1112 uuid = parameters['uuid']
1113 for build in self.running_builds:
1114 if build.unique == uuid:
1115 build.aborted = True
1116 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001117 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001118
James E. Blaira002b032017-04-18 10:35:48 -07001119 def stop(self):
1120 for build in self.running_builds:
1121 build.release()
1122 super(RecordingExecutorServer, self).stop()
1123
Joshua Hesketh50c21782016-10-13 21:34:14 +11001124
Paul Belanger174a8272017-03-14 13:20:10 -04001125class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001126 def doMergeChanges(self, items):
1127 # Get a merger in order to update the repos involved in this job.
1128 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1129 if not commit: # merge conflict
1130 self.recordResult('MERGER_FAILURE')
1131 return commit
1132
1133 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001134 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001135 self.executor_server.lock.acquire()
1136 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001137 BuildHistory(name=build.name, result=result, changes=build.changes,
1138 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001139 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001140 pipeline=build.parameters['ZUUL_PIPELINE'])
1141 )
Paul Belanger174a8272017-03-14 13:20:10 -04001142 self.executor_server.running_builds.remove(build)
1143 del self.executor_server.job_builds[self.job.unique]
1144 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001145
1146 def runPlaybooks(self, args):
1147 build = self.executor_server.job_builds[self.job.unique]
1148 build.jobdir = self.jobdir
1149
1150 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1151 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001152 return result
1153
Monty Taylore6562aa2017-02-20 07:37:39 -05001154 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001155 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001156
Paul Belanger174a8272017-03-14 13:20:10 -04001157 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001158 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001159 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001160 else:
1161 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001162 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001163
James E. Blairad8dca02017-02-21 11:48:32 -05001164 def getHostList(self, args):
1165 self.log.debug("hostlist")
1166 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001167 for host in hosts:
1168 host['host_vars']['ansible_connection'] = 'local'
1169
1170 hosts.append(dict(
1171 name='localhost',
1172 host_vars=dict(ansible_connection='local'),
1173 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001174 return hosts
1175
James E. Blairf5dbd002015-12-23 15:26:17 -08001176
Clark Boylanb640e052014-04-03 16:41:46 -07001177class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001178 """A Gearman server for use in tests.
1179
1180 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1181 added to the queue but will not be distributed to workers
1182 until released. This attribute may be changed at any time and
1183 will take effect for subsequently enqueued jobs, but
1184 previously held jobs will still need to be explicitly
1185 released.
1186
1187 """
1188
Clark Boylanb640e052014-04-03 16:41:46 -07001189 def __init__(self):
1190 self.hold_jobs_in_queue = False
1191 super(FakeGearmanServer, self).__init__(0)
1192
1193 def getJobForConnection(self, connection, peek=False):
1194 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1195 for job in queue:
1196 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001197 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001198 job.waiting = self.hold_jobs_in_queue
1199 else:
1200 job.waiting = False
1201 if job.waiting:
1202 continue
1203 if job.name in connection.functions:
1204 if not peek:
1205 queue.remove(job)
1206 connection.related_jobs[job.handle] = job
1207 job.worker_connection = connection
1208 job.running = True
1209 return job
1210 return None
1211
1212 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001213 """Release a held job.
1214
1215 :arg str regex: A regular expression which, if supplied, will
1216 cause only jobs with matching names to be released. If
1217 not supplied, all jobs will be released.
1218 """
Clark Boylanb640e052014-04-03 16:41:46 -07001219 released = False
1220 qlen = (len(self.high_queue) + len(self.normal_queue) +
1221 len(self.low_queue))
1222 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1223 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001224 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001225 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001226 parameters = json.loads(job.arguments)
1227 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001228 self.log.debug("releasing queued job %s" %
1229 job.unique)
1230 job.waiting = False
1231 released = True
1232 else:
1233 self.log.debug("not releasing queued job %s" %
1234 job.unique)
1235 if released:
1236 self.wakeConnections()
1237 qlen = (len(self.high_queue) + len(self.normal_queue) +
1238 len(self.low_queue))
1239 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1240
1241
1242class FakeSMTP(object):
1243 log = logging.getLogger('zuul.FakeSMTP')
1244
1245 def __init__(self, messages, server, port):
1246 self.server = server
1247 self.port = port
1248 self.messages = messages
1249
1250 def sendmail(self, from_email, to_email, msg):
1251 self.log.info("Sending email from %s, to %s, with msg %s" % (
1252 from_email, to_email, msg))
1253
1254 headers = msg.split('\n\n', 1)[0]
1255 body = msg.split('\n\n', 1)[1]
1256
1257 self.messages.append(dict(
1258 from_email=from_email,
1259 to_email=to_email,
1260 msg=msg,
1261 headers=headers,
1262 body=body,
1263 ))
1264
1265 return True
1266
1267 def quit(self):
1268 return True
1269
1270
James E. Blairdce6cea2016-12-20 16:45:32 -08001271class FakeNodepool(object):
1272 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001273 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001274
1275 log = logging.getLogger("zuul.test.FakeNodepool")
1276
1277 def __init__(self, host, port, chroot):
1278 self.client = kazoo.client.KazooClient(
1279 hosts='%s:%s%s' % (host, port, chroot))
1280 self.client.start()
1281 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001282 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001283 self.thread = threading.Thread(target=self.run)
1284 self.thread.daemon = True
1285 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001286 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001287
1288 def stop(self):
1289 self._running = False
1290 self.thread.join()
1291 self.client.stop()
1292 self.client.close()
1293
1294 def run(self):
1295 while self._running:
1296 self._run()
1297 time.sleep(0.1)
1298
1299 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001300 if self.paused:
1301 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001302 for req in self.getNodeRequests():
1303 self.fulfillRequest(req)
1304
1305 def getNodeRequests(self):
1306 try:
1307 reqids = self.client.get_children(self.REQUEST_ROOT)
1308 except kazoo.exceptions.NoNodeError:
1309 return []
1310 reqs = []
1311 for oid in sorted(reqids):
1312 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001313 try:
1314 data, stat = self.client.get(path)
1315 data = json.loads(data)
1316 data['_oid'] = oid
1317 reqs.append(data)
1318 except kazoo.exceptions.NoNodeError:
1319 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001320 return reqs
1321
James E. Blaire18d4602017-01-05 11:17:28 -08001322 def getNodes(self):
1323 try:
1324 nodeids = self.client.get_children(self.NODE_ROOT)
1325 except kazoo.exceptions.NoNodeError:
1326 return []
1327 nodes = []
1328 for oid in sorted(nodeids):
1329 path = self.NODE_ROOT + '/' + oid
1330 data, stat = self.client.get(path)
1331 data = json.loads(data)
1332 data['_oid'] = oid
1333 try:
1334 lockfiles = self.client.get_children(path + '/lock')
1335 except kazoo.exceptions.NoNodeError:
1336 lockfiles = []
1337 if lockfiles:
1338 data['_lock'] = True
1339 else:
1340 data['_lock'] = False
1341 nodes.append(data)
1342 return nodes
1343
James E. Blaira38c28e2017-01-04 10:33:20 -08001344 def makeNode(self, request_id, node_type):
1345 now = time.time()
1346 path = '/nodepool/nodes/'
1347 data = dict(type=node_type,
1348 provider='test-provider',
1349 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001350 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001351 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001352 public_ipv4='127.0.0.1',
1353 private_ipv4=None,
1354 public_ipv6=None,
1355 allocated_to=request_id,
1356 state='ready',
1357 state_time=now,
1358 created_time=now,
1359 updated_time=now,
1360 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001361 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001362 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001363 data = json.dumps(data)
1364 path = self.client.create(path, data,
1365 makepath=True,
1366 sequence=True)
1367 nodeid = path.split("/")[-1]
1368 return nodeid
1369
James E. Blair6ab79e02017-01-06 10:10:17 -08001370 def addFailRequest(self, request):
1371 self.fail_requests.add(request['_oid'])
1372
James E. Blairdce6cea2016-12-20 16:45:32 -08001373 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001374 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001375 return
1376 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001377 oid = request['_oid']
1378 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001379
James E. Blair6ab79e02017-01-06 10:10:17 -08001380 if oid in self.fail_requests:
1381 request['state'] = 'failed'
1382 else:
1383 request['state'] = 'fulfilled'
1384 nodes = []
1385 for node in request['node_types']:
1386 nodeid = self.makeNode(oid, node)
1387 nodes.append(nodeid)
1388 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001389
James E. Blaira38c28e2017-01-04 10:33:20 -08001390 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001391 path = self.REQUEST_ROOT + '/' + oid
1392 data = json.dumps(request)
1393 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1394 self.client.set(path, data)
1395
1396
James E. Blair498059b2016-12-20 13:50:13 -08001397class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001398 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001399 super(ChrootedKazooFixture, self).__init__()
1400
1401 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1402 if ':' in zk_host:
1403 host, port = zk_host.split(':')
1404 else:
1405 host = zk_host
1406 port = None
1407
1408 self.zookeeper_host = host
1409
1410 if not port:
1411 self.zookeeper_port = 2181
1412 else:
1413 self.zookeeper_port = int(port)
1414
Clark Boylan621ec9a2017-04-07 17:41:33 -07001415 self.test_id = test_id
1416
James E. Blair498059b2016-12-20 13:50:13 -08001417 def _setUp(self):
1418 # Make sure the test chroot paths do not conflict
1419 random_bits = ''.join(random.choice(string.ascii_lowercase +
1420 string.ascii_uppercase)
1421 for x in range(8))
1422
Clark Boylan621ec9a2017-04-07 17:41:33 -07001423 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001424 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1425
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001426 self.addCleanup(self._cleanup)
1427
James E. Blair498059b2016-12-20 13:50:13 -08001428 # Ensure the chroot path exists and clean up any pre-existing znodes.
1429 _tmp_client = kazoo.client.KazooClient(
1430 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1431 _tmp_client.start()
1432
1433 if _tmp_client.exists(self.zookeeper_chroot):
1434 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1435
1436 _tmp_client.ensure_path(self.zookeeper_chroot)
1437 _tmp_client.stop()
1438 _tmp_client.close()
1439
James E. Blair498059b2016-12-20 13:50:13 -08001440 def _cleanup(self):
1441 '''Remove the chroot path.'''
1442 # Need a non-chroot'ed client to remove the chroot path
1443 _tmp_client = kazoo.client.KazooClient(
1444 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1445 _tmp_client.start()
1446 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1447 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001448 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001449
1450
Joshua Heskethd78b4482015-09-14 16:56:34 -06001451class MySQLSchemaFixture(fixtures.Fixture):
1452 def setUp(self):
1453 super(MySQLSchemaFixture, self).setUp()
1454
1455 random_bits = ''.join(random.choice(string.ascii_lowercase +
1456 string.ascii_uppercase)
1457 for x in range(8))
1458 self.name = '%s_%s' % (random_bits, os.getpid())
1459 self.passwd = uuid.uuid4().hex
1460 db = pymysql.connect(host="localhost",
1461 user="openstack_citest",
1462 passwd="openstack_citest",
1463 db="openstack_citest")
1464 cur = db.cursor()
1465 cur.execute("create database %s" % self.name)
1466 cur.execute(
1467 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1468 (self.name, self.name, self.passwd))
1469 cur.execute("flush privileges")
1470
1471 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1472 self.passwd,
1473 self.name)
1474 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1475 self.addCleanup(self.cleanup)
1476
1477 def cleanup(self):
1478 db = pymysql.connect(host="localhost",
1479 user="openstack_citest",
1480 passwd="openstack_citest",
1481 db="openstack_citest")
1482 cur = db.cursor()
1483 cur.execute("drop database %s" % self.name)
1484 cur.execute("drop user '%s'@'localhost'" % self.name)
1485 cur.execute("flush privileges")
1486
1487
Maru Newby3fe5f852015-01-13 04:22:14 +00001488class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001489 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001490 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001491
James E. Blair1c236df2017-02-01 14:07:24 -08001492 def attachLogs(self, *args):
1493 def reader():
1494 self._log_stream.seek(0)
1495 while True:
1496 x = self._log_stream.read(4096)
1497 if not x:
1498 break
1499 yield x.encode('utf8')
1500 content = testtools.content.content_from_reader(
1501 reader,
1502 testtools.content_type.UTF8_TEXT,
1503 False)
1504 self.addDetail('logging', content)
1505
Clark Boylanb640e052014-04-03 16:41:46 -07001506 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001507 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001508 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1509 try:
1510 test_timeout = int(test_timeout)
1511 except ValueError:
1512 # If timeout value is invalid do not set a timeout.
1513 test_timeout = 0
1514 if test_timeout > 0:
1515 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1516
1517 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1518 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1519 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1520 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1521 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1522 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1523 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1524 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1525 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1526 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001527 self._log_stream = StringIO()
1528 self.addOnException(self.attachLogs)
1529 else:
1530 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001531
James E. Blair1c236df2017-02-01 14:07:24 -08001532 handler = logging.StreamHandler(self._log_stream)
1533 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1534 '%(levelname)-8s %(message)s')
1535 handler.setFormatter(formatter)
1536
1537 logger = logging.getLogger()
1538 logger.setLevel(logging.DEBUG)
1539 logger.addHandler(handler)
1540
Clark Boylan3410d532017-04-25 12:35:29 -07001541 # Make sure we don't carry old handlers around in process state
1542 # which slows down test runs
1543 self.addCleanup(logger.removeHandler, handler)
1544 self.addCleanup(handler.close)
1545 self.addCleanup(handler.flush)
1546
James E. Blair1c236df2017-02-01 14:07:24 -08001547 # NOTE(notmorgan): Extract logging overrides for specific
1548 # libraries from the OS_LOG_DEFAULTS env and create loggers
1549 # for each. This is used to limit the output during test runs
1550 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001551 log_defaults_from_env = os.environ.get(
1552 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001553 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001554
James E. Blairdce6cea2016-12-20 16:45:32 -08001555 if log_defaults_from_env:
1556 for default in log_defaults_from_env.split(','):
1557 try:
1558 name, level_str = default.split('=', 1)
1559 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001560 logger = logging.getLogger(name)
1561 logger.setLevel(level)
1562 logger.addHandler(handler)
1563 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001564 except ValueError:
1565 # NOTE(notmorgan): Invalid format of the log default,
1566 # skip and don't try and apply a logger for the
1567 # specified module
1568 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001569
Maru Newby3fe5f852015-01-13 04:22:14 +00001570
1571class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001572 """A test case with a functioning Zuul.
1573
1574 The following class variables are used during test setup and can
1575 be overidden by subclasses but are effectively read-only once a
1576 test method starts running:
1577
1578 :cvar str config_file: This points to the main zuul config file
1579 within the fixtures directory. Subclasses may override this
1580 to obtain a different behavior.
1581
1582 :cvar str tenant_config_file: This is the tenant config file
1583 (which specifies from what git repos the configuration should
1584 be loaded). It defaults to the value specified in
1585 `config_file` but can be overidden by subclasses to obtain a
1586 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001587 configuration. See also the :py:func:`simple_layout`
1588 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001589
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001590 :cvar bool create_project_keys: Indicates whether Zuul should
1591 auto-generate keys for each project, or whether the test
1592 infrastructure should insert dummy keys to save time during
1593 startup. Defaults to False.
1594
James E. Blaire7b99a02016-08-05 14:27:34 -07001595 The following are instance variables that are useful within test
1596 methods:
1597
1598 :ivar FakeGerritConnection fake_<connection>:
1599 A :py:class:`~tests.base.FakeGerritConnection` will be
1600 instantiated for each connection present in the config file
1601 and stored here. For instance, `fake_gerrit` will hold the
1602 FakeGerritConnection object for a connection named `gerrit`.
1603
1604 :ivar FakeGearmanServer gearman_server: An instance of
1605 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1606 server that all of the Zuul components in this test use to
1607 communicate with each other.
1608
Paul Belanger174a8272017-03-14 13:20:10 -04001609 :ivar RecordingExecutorServer executor_server: An instance of
1610 :py:class:`~tests.base.RecordingExecutorServer` which is the
1611 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001612
1613 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1614 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001615 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001616 list upon completion.
1617
1618 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1619 objects representing completed builds. They are appended to
1620 the list in the order they complete.
1621
1622 """
1623
James E. Blair83005782015-12-11 14:46:03 -08001624 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001625 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001626 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001627
1628 def _startMerger(self):
1629 self.merge_server = zuul.merger.server.MergeServer(self.config,
1630 self.connections)
1631 self.merge_server.start()
1632
Maru Newby3fe5f852015-01-13 04:22:14 +00001633 def setUp(self):
1634 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001635
1636 self.setupZK()
1637
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001638 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001639 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001640 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1641 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001642 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001643 tmp_root = tempfile.mkdtemp(
1644 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001645 self.test_root = os.path.join(tmp_root, "zuul-test")
1646 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001647 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001648 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001649 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001650
1651 if os.path.exists(self.test_root):
1652 shutil.rmtree(self.test_root)
1653 os.makedirs(self.test_root)
1654 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001655 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001656
1657 # Make per test copy of Configuration.
1658 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001659 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001660 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001661 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001662 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001663 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001664 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001665
Clark Boylanb640e052014-04-03 16:41:46 -07001666 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001667 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1668 # see: https://github.com/jsocol/pystatsd/issues/61
1669 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001670 os.environ['STATSD_PORT'] = str(self.statsd.port)
1671 self.statsd.start()
1672 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001673 reload_module(statsd)
1674 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001675
1676 self.gearman_server = FakeGearmanServer()
1677
1678 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001679 self.log.info("Gearman server on port %s" %
1680 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001681
James E. Blaire511d2f2016-12-08 15:22:26 -08001682 gerritsource.GerritSource.replication_timeout = 1.5
1683 gerritsource.GerritSource.replication_retry_interval = 0.5
1684 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001685
Joshua Hesketh352264b2015-08-11 23:42:08 +10001686 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001687
Jan Hruban7083edd2015-08-21 14:00:54 +02001688 self.webapp = zuul.webapp.WebApp(
1689 self.sched, port=0, listen_address='127.0.0.1')
1690
Jan Hruban6b71aff2015-10-22 16:58:08 +02001691 self.event_queues = [
1692 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001693 self.sched.trigger_event_queue,
1694 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001695 ]
1696
James E. Blairfef78942016-03-11 16:28:56 -08001697 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001698 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001699
Clark Boylanb640e052014-04-03 16:41:46 -07001700 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001701 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001702 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001703 return FakeURLOpener(self.upstream_root, *args, **kw)
1704
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001705 old_urlopen = urllib.request.urlopen
1706 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001707
James E. Blair3f876d52016-07-22 13:07:14 -07001708 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001709
Paul Belanger174a8272017-03-14 13:20:10 -04001710 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001711 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001712 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001713 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001714 _test_root=self.test_root,
1715 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001716 self.executor_server.start()
1717 self.history = self.executor_server.build_history
1718 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001719
Paul Belanger174a8272017-03-14 13:20:10 -04001720 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001721 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001722 self.merge_client = zuul.merger.client.MergeClient(
1723 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001724 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001725 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001726 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001727
James E. Blair0d5a36e2017-02-21 10:53:44 -05001728 self.fake_nodepool = FakeNodepool(
1729 self.zk_chroot_fixture.zookeeper_host,
1730 self.zk_chroot_fixture.zookeeper_port,
1731 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001732
Paul Belanger174a8272017-03-14 13:20:10 -04001733 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001734 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001735 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001736 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001737
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001738 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001739
1740 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001741 self.webapp.start()
1742 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001743 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001744 # Cleanups are run in reverse order
1745 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001746 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001747 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001748
James E. Blairb9c0d772017-03-03 14:34:49 -08001749 self.sched.reconfigure(self.config)
1750 self.sched.resume()
1751
James E. Blairfef78942016-03-11 16:28:56 -08001752 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001753 # Set up gerrit related fakes
1754 # Set a changes database so multiple FakeGerrit's can report back to
1755 # a virtual canonical database given by the configured hostname
1756 self.gerrit_changes_dbs = {}
1757
1758 def getGerritConnection(driver, name, config):
1759 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1760 con = FakeGerritConnection(driver, name, config,
1761 changes_db=db,
1762 upstream_root=self.upstream_root)
1763 self.event_queues.append(con.event_queue)
1764 setattr(self, 'fake_' + name, con)
1765 return con
1766
1767 self.useFixture(fixtures.MonkeyPatch(
1768 'zuul.driver.gerrit.GerritDriver.getConnection',
1769 getGerritConnection))
1770
Gregory Haynes4fc12542015-04-22 20:38:06 -07001771 def getGithubConnection(driver, name, config):
1772 con = FakeGithubConnection(driver, name, config,
1773 upstream_root=self.upstream_root)
1774 setattr(self, 'fake_' + name, con)
1775 return con
1776
1777 self.useFixture(fixtures.MonkeyPatch(
1778 'zuul.driver.github.GithubDriver.getConnection',
1779 getGithubConnection))
1780
James E. Blaire511d2f2016-12-08 15:22:26 -08001781 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001782 # TODO(jhesketh): This should come from lib.connections for better
1783 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001784 # Register connections from the config
1785 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001786
Joshua Hesketh352264b2015-08-11 23:42:08 +10001787 def FakeSMTPFactory(*args, **kw):
1788 args = [self.smtp_messages] + list(args)
1789 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001790
Joshua Hesketh352264b2015-08-11 23:42:08 +10001791 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001792
James E. Blaire511d2f2016-12-08 15:22:26 -08001793 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001794 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001795 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001796
James E. Blair83005782015-12-11 14:46:03 -08001797 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001798 # This creates the per-test configuration object. It can be
1799 # overriden by subclasses, but should not need to be since it
1800 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001801 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001802 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001803
1804 if not self.setupSimpleLayout():
1805 if hasattr(self, 'tenant_config_file'):
1806 self.config.set('zuul', 'tenant_config',
1807 self.tenant_config_file)
1808 git_path = os.path.join(
1809 os.path.dirname(
1810 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1811 'git')
1812 if os.path.exists(git_path):
1813 for reponame in os.listdir(git_path):
1814 project = reponame.replace('_', '/')
1815 self.copyDirToRepo(project,
1816 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001817 self.setupAllProjectKeys()
1818
James E. Blair06cc3922017-04-19 10:08:10 -07001819 def setupSimpleLayout(self):
1820 # If the test method has been decorated with a simple_layout,
1821 # use that instead of the class tenant_config_file. Set up a
1822 # single config-project with the specified layout, and
1823 # initialize repos for all of the 'project' entries which
1824 # appear in the layout.
1825 test_name = self.id().split('.')[-1]
1826 test = getattr(self, test_name)
1827 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001828 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001829 else:
1830 return False
1831
James E. Blairb70e55a2017-04-19 12:57:02 -07001832 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001833 path = os.path.join(FIXTURE_DIR, path)
1834 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001835 data = f.read()
1836 layout = yaml.safe_load(data)
1837 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001838 untrusted_projects = []
1839 for item in layout:
1840 if 'project' in item:
1841 name = item['project']['name']
1842 untrusted_projects.append(name)
1843 self.init_repo(name)
1844 self.addCommitToRepo(name, 'initial commit',
1845 files={'README': ''},
1846 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001847 if 'job' in item:
1848 jobname = item['job']['name']
1849 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001850
1851 root = os.path.join(self.test_root, "config")
1852 if not os.path.exists(root):
1853 os.makedirs(root)
1854 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1855 config = [{'tenant':
1856 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001857 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001858 {'config-projects': ['common-config'],
1859 'untrusted-projects': untrusted_projects}}}}]
1860 f.write(yaml.dump(config))
1861 f.close()
1862 self.config.set('zuul', 'tenant_config',
1863 os.path.join(FIXTURE_DIR, f.name))
1864
1865 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001866 self.addCommitToRepo('common-config', 'add content from fixture',
1867 files, branch='master', tag='init')
1868
1869 return True
1870
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001871 def setupAllProjectKeys(self):
1872 if self.create_project_keys:
1873 return
1874
1875 path = self.config.get('zuul', 'tenant_config')
1876 with open(os.path.join(FIXTURE_DIR, path)) as f:
1877 tenant_config = yaml.safe_load(f.read())
1878 for tenant in tenant_config:
1879 sources = tenant['tenant']['source']
1880 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001881 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001882 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001883 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001884 self.setupProjectKeys(source, project)
1885
1886 def setupProjectKeys(self, source, project):
1887 # Make sure we set up an RSA key for the project so that we
1888 # don't spend time generating one:
1889
1890 key_root = os.path.join(self.state_root, 'keys')
1891 if not os.path.isdir(key_root):
1892 os.mkdir(key_root, 0o700)
1893 private_key_file = os.path.join(key_root, source, project + '.pem')
1894 private_key_dir = os.path.dirname(private_key_file)
1895 self.log.debug("Installing test keys for project %s at %s" % (
1896 project, private_key_file))
1897 if not os.path.isdir(private_key_dir):
1898 os.makedirs(private_key_dir)
1899 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1900 with open(private_key_file, 'w') as o:
1901 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001902
James E. Blair498059b2016-12-20 13:50:13 -08001903 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001904 self.zk_chroot_fixture = self.useFixture(
1905 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001906 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001907 self.zk_chroot_fixture.zookeeper_host,
1908 self.zk_chroot_fixture.zookeeper_port,
1909 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001910
James E. Blair96c6bf82016-01-15 16:20:40 -08001911 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001912 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001913
1914 files = {}
1915 for (dirpath, dirnames, filenames) in os.walk(source_path):
1916 for filename in filenames:
1917 test_tree_filepath = os.path.join(dirpath, filename)
1918 common_path = os.path.commonprefix([test_tree_filepath,
1919 source_path])
1920 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1921 with open(test_tree_filepath, 'r') as f:
1922 content = f.read()
1923 files[relative_filepath] = content
1924 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001925 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001926
James E. Blaire18d4602017-01-05 11:17:28 -08001927 def assertNodepoolState(self):
1928 # Make sure that there are no pending requests
1929
1930 requests = self.fake_nodepool.getNodeRequests()
1931 self.assertEqual(len(requests), 0)
1932
1933 nodes = self.fake_nodepool.getNodes()
1934 for node in nodes:
1935 self.assertFalse(node['_lock'], "Node %s is locked" %
1936 (node['_oid'],))
1937
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001938 def assertNoGeneratedKeys(self):
1939 # Make sure that Zuul did not generate any project keys
1940 # (unless it was supposed to).
1941
1942 if self.create_project_keys:
1943 return
1944
1945 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1946 test_key = i.read()
1947
1948 key_root = os.path.join(self.state_root, 'keys')
1949 for root, dirname, files in os.walk(key_root):
1950 for fn in files:
1951 with open(os.path.join(root, fn)) as f:
1952 self.assertEqual(test_key, f.read())
1953
Clark Boylanb640e052014-04-03 16:41:46 -07001954 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001955 self.log.debug("Assert final state")
1956 # Make sure no jobs are running
1957 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001958 # Make sure that git.Repo objects have been garbage collected.
1959 repos = []
1960 gc.collect()
1961 for obj in gc.get_objects():
1962 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001963 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001964 repos.append(obj)
1965 self.assertEqual(len(repos), 0)
1966 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001967 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001968 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001969 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001970 for tenant in self.sched.abide.tenants.values():
1971 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001972 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001973 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001974
1975 def shutdown(self):
1976 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001977 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001978 self.merge_server.stop()
1979 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001980 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001981 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001982 self.sched.stop()
1983 self.sched.join()
1984 self.statsd.stop()
1985 self.statsd.join()
1986 self.webapp.stop()
1987 self.webapp.join()
1988 self.rpc.stop()
1989 self.rpc.join()
1990 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001991 self.fake_nodepool.stop()
1992 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001993 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001994 # we whitelist watchdog threads as they have relatively long delays
1995 # before noticing they should exit, but they should exit on their own.
1996 threads = [t for t in threading.enumerate()
1997 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07001998 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07001999 log_str = ""
2000 for thread_id, stack_frame in sys._current_frames().items():
2001 log_str += "Thread: %s\n" % thread_id
2002 log_str += "".join(traceback.format_stack(stack_frame))
2003 self.log.debug(log_str)
2004 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002005
James E. Blaira002b032017-04-18 10:35:48 -07002006 def assertCleanShutdown(self):
2007 pass
2008
James E. Blairc4ba97a2017-04-19 16:26:24 -07002009 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002010 parts = project.split('/')
2011 path = os.path.join(self.upstream_root, *parts[:-1])
2012 if not os.path.exists(path):
2013 os.makedirs(path)
2014 path = os.path.join(self.upstream_root, project)
2015 repo = git.Repo.init(path)
2016
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002017 with repo.config_writer() as config_writer:
2018 config_writer.set_value('user', 'email', 'user@example.com')
2019 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002020
Clark Boylanb640e052014-04-03 16:41:46 -07002021 repo.index.commit('initial commit')
2022 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002023 if tag:
2024 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002025
James E. Blair97d902e2014-08-21 13:25:56 -07002026 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002027 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002028 repo.git.clean('-x', '-f', '-d')
2029
James E. Blair97d902e2014-08-21 13:25:56 -07002030 def create_branch(self, project, branch):
2031 path = os.path.join(self.upstream_root, project)
2032 repo = git.Repo.init(path)
2033 fn = os.path.join(path, 'README')
2034
2035 branch_head = repo.create_head(branch)
2036 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002037 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002038 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002039 f.close()
2040 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002041 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002042
James E. Blair97d902e2014-08-21 13:25:56 -07002043 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002044 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002045 repo.git.clean('-x', '-f', '-d')
2046
Sachi King9f16d522016-03-16 12:20:45 +11002047 def create_commit(self, project):
2048 path = os.path.join(self.upstream_root, project)
2049 repo = git.Repo(path)
2050 repo.head.reference = repo.heads['master']
2051 file_name = os.path.join(path, 'README')
2052 with open(file_name, 'a') as f:
2053 f.write('creating fake commit\n')
2054 repo.index.add([file_name])
2055 commit = repo.index.commit('Creating a fake commit')
2056 return commit.hexsha
2057
James E. Blairf4a5f022017-04-18 14:01:10 -07002058 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002059 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002060 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002061 while len(self.builds):
2062 self.release(self.builds[0])
2063 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002064 i += 1
2065 if count is not None and i >= count:
2066 break
James E. Blairb8c16472015-05-05 14:55:26 -07002067
Clark Boylanb640e052014-04-03 16:41:46 -07002068 def release(self, job):
2069 if isinstance(job, FakeBuild):
2070 job.release()
2071 else:
2072 job.waiting = False
2073 self.log.debug("Queued job %s released" % job.unique)
2074 self.gearman_server.wakeConnections()
2075
2076 def getParameter(self, job, name):
2077 if isinstance(job, FakeBuild):
2078 return job.parameters[name]
2079 else:
2080 parameters = json.loads(job.arguments)
2081 return parameters[name]
2082
Clark Boylanb640e052014-04-03 16:41:46 -07002083 def haveAllBuildsReported(self):
2084 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002085 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002086 return False
2087 # Find out if every build that the worker has completed has been
2088 # reported back to Zuul. If it hasn't then that means a Gearman
2089 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002090 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002091 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002092 if not zbuild:
2093 # It has already been reported
2094 continue
2095 # It hasn't been reported yet.
2096 return False
2097 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002098 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002099 if connection.state == 'GRAB_WAIT':
2100 return False
2101 return True
2102
2103 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002104 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002105 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002106 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002107 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002108 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002109 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002110 for j in conn.related_jobs.values():
2111 if j.unique == build.uuid:
2112 client_job = j
2113 break
2114 if not client_job:
2115 self.log.debug("%s is not known to the gearman client" %
2116 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002117 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002118 if not client_job.handle:
2119 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002120 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002121 server_job = self.gearman_server.jobs.get(client_job.handle)
2122 if not server_job:
2123 self.log.debug("%s is not known to the gearman server" %
2124 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002125 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002126 if not hasattr(server_job, 'waiting'):
2127 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002128 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002129 if server_job.waiting:
2130 continue
James E. Blair17302972016-08-10 16:11:42 -07002131 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002132 self.log.debug("%s has not reported start" % build)
2133 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002134 worker_build = self.executor_server.job_builds.get(
2135 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002136 if worker_build:
2137 if worker_build.isWaiting():
2138 continue
2139 else:
2140 self.log.debug("%s is running" % worker_build)
2141 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002142 else:
James E. Blair962220f2016-08-03 11:22:38 -07002143 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002144 return False
James E. Blaira002b032017-04-18 10:35:48 -07002145 for (build_uuid, job_worker) in \
2146 self.executor_server.job_workers.items():
2147 if build_uuid not in seen_builds:
2148 self.log.debug("%s is not finalized" % build_uuid)
2149 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002150 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002151
James E. Blairdce6cea2016-12-20 16:45:32 -08002152 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002153 if self.fake_nodepool.paused:
2154 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002155 if self.sched.nodepool.requests:
2156 return False
2157 return True
2158
Jan Hruban6b71aff2015-10-22 16:58:08 +02002159 def eventQueuesEmpty(self):
2160 for queue in self.event_queues:
2161 yield queue.empty()
2162
2163 def eventQueuesJoin(self):
2164 for queue in self.event_queues:
2165 queue.join()
2166
Clark Boylanb640e052014-04-03 16:41:46 -07002167 def waitUntilSettled(self):
2168 self.log.debug("Waiting until settled...")
2169 start = time.time()
2170 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002171 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002172 self.log.error("Timeout waiting for Zuul to settle")
2173 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002174 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002175 self.log.error(" %s: %s" % (queue, queue.empty()))
2176 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002177 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002178 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002179 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002180 self.log.error("All requests completed: %s" %
2181 (self.areAllNodeRequestsComplete(),))
2182 self.log.error("Merge client jobs: %s" %
2183 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002184 raise Exception("Timeout waiting for Zuul to settle")
2185 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002186
Paul Belanger174a8272017-03-14 13:20:10 -04002187 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002188 # have all build states propogated to zuul?
2189 if self.haveAllBuildsReported():
2190 # Join ensures that the queue is empty _and_ events have been
2191 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002192 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002193 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002194 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002195 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002196 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002197 self.areAllNodeRequestsComplete() and
2198 all(self.eventQueuesEmpty())):
2199 # The queue empty check is placed at the end to
2200 # ensure that if a component adds an event between
2201 # when locked the run handler and checked that the
2202 # components were stable, we don't erroneously
2203 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002204 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002205 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002206 self.log.debug("...settled.")
2207 return
2208 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002209 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002210 self.sched.wake_event.wait(0.1)
2211
2212 def countJobResults(self, jobs, result):
2213 jobs = filter(lambda x: x.result == result, jobs)
2214 return len(jobs)
2215
James E. Blair96c6bf82016-01-15 16:20:40 -08002216 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002217 for job in self.history:
2218 if (job.name == name and
2219 (project is None or
2220 job.parameters['ZUUL_PROJECT'] == project)):
2221 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002222 raise Exception("Unable to find job %s in history" % name)
2223
2224 def assertEmptyQueues(self):
2225 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002226 for tenant in self.sched.abide.tenants.values():
2227 for pipeline in tenant.layout.pipelines.values():
2228 for queue in pipeline.queues:
2229 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002230 print('pipeline %s queue %s contents %s' % (
2231 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002232 self.assertEqual(len(queue.queue), 0,
2233 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002234
2235 def assertReportedStat(self, key, value=None, kind=None):
2236 start = time.time()
2237 while time.time() < (start + 5):
2238 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002239 k, v = stat.split(':')
2240 if key == k:
2241 if value is None and kind is None:
2242 return
2243 elif value:
2244 if value == v:
2245 return
2246 elif kind:
2247 if v.endswith('|' + kind):
2248 return
2249 time.sleep(0.1)
2250
Clark Boylanb640e052014-04-03 16:41:46 -07002251 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002252
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002253 def assertBuilds(self, builds):
2254 """Assert that the running builds are as described.
2255
2256 The list of running builds is examined and must match exactly
2257 the list of builds described by the input.
2258
2259 :arg list builds: A list of dictionaries. Each item in the
2260 list must match the corresponding build in the build
2261 history, and each element of the dictionary must match the
2262 corresponding attribute of the build.
2263
2264 """
James E. Blair3158e282016-08-19 09:34:11 -07002265 try:
2266 self.assertEqual(len(self.builds), len(builds))
2267 for i, d in enumerate(builds):
2268 for k, v in d.items():
2269 self.assertEqual(
2270 getattr(self.builds[i], k), v,
2271 "Element %i in builds does not match" % (i,))
2272 except Exception:
2273 for build in self.builds:
2274 self.log.error("Running build: %s" % build)
2275 else:
2276 self.log.error("No running builds")
2277 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002278
James E. Blairb536ecc2016-08-31 10:11:42 -07002279 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002280 """Assert that the completed builds are as described.
2281
2282 The list of completed builds is examined and must match
2283 exactly the list of builds described by the input.
2284
2285 :arg list history: A list of dictionaries. Each item in the
2286 list must match the corresponding build in the build
2287 history, and each element of the dictionary must match the
2288 corresponding attribute of the build.
2289
James E. Blairb536ecc2016-08-31 10:11:42 -07002290 :arg bool ordered: If true, the history must match the order
2291 supplied, if false, the builds are permitted to have
2292 arrived in any order.
2293
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002294 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002295 def matches(history_item, item):
2296 for k, v in item.items():
2297 if getattr(history_item, k) != v:
2298 return False
2299 return True
James E. Blair3158e282016-08-19 09:34:11 -07002300 try:
2301 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002302 if ordered:
2303 for i, d in enumerate(history):
2304 if not matches(self.history[i], d):
2305 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002306 "Element %i in history does not match %s" %
2307 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002308 else:
2309 unseen = self.history[:]
2310 for i, d in enumerate(history):
2311 found = False
2312 for unseen_item in unseen:
2313 if matches(unseen_item, d):
2314 found = True
2315 unseen.remove(unseen_item)
2316 break
2317 if not found:
2318 raise Exception("No match found for element %i "
2319 "in history" % (i,))
2320 if unseen:
2321 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002322 except Exception:
2323 for build in self.history:
2324 self.log.error("Completed build: %s" % build)
2325 else:
2326 self.log.error("No completed builds")
2327 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002328
James E. Blair6ac368c2016-12-22 18:07:20 -08002329 def printHistory(self):
2330 """Log the build history.
2331
2332 This can be useful during tests to summarize what jobs have
2333 completed.
2334
2335 """
2336 self.log.debug("Build history:")
2337 for build in self.history:
2338 self.log.debug(build)
2339
James E. Blair59fdbac2015-12-07 17:08:06 -08002340 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002341 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2342
James E. Blair9ea70072017-04-19 16:05:30 -07002343 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002344 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002345 if not os.path.exists(root):
2346 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002347 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2348 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002349- tenant:
2350 name: openstack
2351 source:
2352 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002353 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002354 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002355 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002356 - org/project
2357 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002358 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002359 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002360 self.config.set('zuul', 'tenant_config',
2361 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002362 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002363
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002364 def addCommitToRepo(self, project, message, files,
2365 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002366 path = os.path.join(self.upstream_root, project)
2367 repo = git.Repo(path)
2368 repo.head.reference = branch
2369 zuul.merger.merger.reset_repo_to_head(repo)
2370 for fn, content in files.items():
2371 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002372 try:
2373 os.makedirs(os.path.dirname(fn))
2374 except OSError:
2375 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002376 with open(fn, 'w') as f:
2377 f.write(content)
2378 repo.index.add([fn])
2379 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002380 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002381 repo.heads[branch].commit = commit
2382 repo.head.reference = branch
2383 repo.git.clean('-x', '-f', '-d')
2384 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002385 if tag:
2386 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002387 return before
2388
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002389 def commitConfigUpdate(self, project_name, source_name):
2390 """Commit an update to zuul.yaml
2391
2392 This overwrites the zuul.yaml in the specificed project with
2393 the contents specified.
2394
2395 :arg str project_name: The name of the project containing
2396 zuul.yaml (e.g., common-config)
2397
2398 :arg str source_name: The path to the file (underneath the
2399 test fixture directory) whose contents should be used to
2400 replace zuul.yaml.
2401 """
2402
2403 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002404 files = {}
2405 with open(source_path, 'r') as f:
2406 data = f.read()
2407 layout = yaml.safe_load(data)
2408 files['zuul.yaml'] = data
2409 for item in layout:
2410 if 'job' in item:
2411 jobname = item['job']['name']
2412 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002413 before = self.addCommitToRepo(
2414 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002415 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002416 return before
2417
James E. Blair7fc8daa2016-08-08 15:37:15 -07002418 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002419
James E. Blair7fc8daa2016-08-08 15:37:15 -07002420 """Inject a Fake (Gerrit) event.
2421
2422 This method accepts a JSON-encoded event and simulates Zuul
2423 having received it from Gerrit. It could (and should)
2424 eventually apply to any connection type, but is currently only
2425 used with Gerrit connections. The name of the connection is
2426 used to look up the corresponding server, and the event is
2427 simulated as having been received by all Zuul connections
2428 attached to that server. So if two Gerrit connections in Zuul
2429 are connected to the same Gerrit server, and you invoke this
2430 method specifying the name of one of them, the event will be
2431 received by both.
2432
2433 .. note::
2434
2435 "self.fake_gerrit.addEvent" calls should be migrated to
2436 this method.
2437
2438 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002439 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002440 :arg str event: The JSON-encoded event.
2441
2442 """
2443 specified_conn = self.connections.connections[connection]
2444 for conn in self.connections.connections.values():
2445 if (isinstance(conn, specified_conn.__class__) and
2446 specified_conn.server == conn.server):
2447 conn.addEvent(event)
2448
James E. Blair3f876d52016-07-22 13:07:14 -07002449
2450class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002451 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002452 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002453
Joshua Heskethd78b4482015-09-14 16:56:34 -06002454
2455class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002456 def setup_config(self):
2457 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002458 for section_name in self.config.sections():
2459 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2460 section_name, re.I)
2461 if not con_match:
2462 continue
2463
2464 if self.config.get(section_name, 'driver') == 'sql':
2465 f = MySQLSchemaFixture()
2466 self.useFixture(f)
2467 if (self.config.get(section_name, 'dburi') ==
2468 '$MYSQL_FIXTURE_DBURI$'):
2469 self.config.set(section_name, 'dburi', f.dburi)