blob: 77c0644df584f4da3b130f7225ca7f6fcca350cc [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,
Jan Hruban37615e52015-11-19 14:30:49 +0100550 subject, upstream_root, number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551 """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
Jan Hruban37615e52015-11-19 14:30:49 +0100558 self.subject = subject
559 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.upstream_root = upstream_root
561 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100562 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100563 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self.updated_at = None
565 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100566 self.is_merged = False
Gregory Haynes4fc12542015-04-22 20:38:06 -0700567 self._createPRRef()
568 self._addCommitToRepo()
569 self._updateTimeStamp()
570
571 def addCommit(self):
572 """Adds a commit on top of the actual PR head."""
573 self._addCommitToRepo()
574 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100575 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576
577 def forcePush(self):
578 """Clears actual commits and add a commit on top of the base."""
579 self._addCommitToRepo(reset=True)
580 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100581 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582
583 def getPullRequestOpenedEvent(self):
584 return self._getPullRequestEvent('opened')
585
586 def getPullRequestSynchronizeEvent(self):
587 return self._getPullRequestEvent('synchronize')
588
589 def getPullRequestReopenedEvent(self):
590 return self._getPullRequestEvent('reopened')
591
592 def getPullRequestClosedEvent(self):
593 return self._getPullRequestEvent('closed')
594
595 def addComment(self, message):
596 self.comments.append(message)
597 self._updateTimeStamp()
598
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200599 def getCommentAddedEvent(self, text):
600 name = 'issue_comment'
601 data = {
602 'action': 'created',
603 'issue': {
604 'number': self.number
605 },
606 'comment': {
607 'body': text
608 },
609 'repository': {
610 'full_name': self.project
611 }
612 }
613 return (name, data)
614
Jan Hruban16ad31f2015-11-07 14:39:07 +0100615 def addLabel(self, name):
616 if name not in self.labels:
617 self.labels.append(name)
618 self._updateTimeStamp()
619 return self._getLabelEvent(name)
620
621 def removeLabel(self, name):
622 if name in self.labels:
623 self.labels.remove(name)
624 self._updateTimeStamp()
625 return self._getUnlabelEvent(name)
626
627 def _getLabelEvent(self, label):
628 name = 'pull_request'
629 data = {
630 'action': 'labeled',
631 'pull_request': {
632 'number': self.number,
633 'updated_at': self.updated_at,
634 'base': {
635 'ref': self.branch,
636 'repo': {
637 'full_name': self.project
638 }
639 },
640 'head': {
641 'sha': self.head_sha
642 }
643 },
644 'label': {
645 'name': label
646 }
647 }
648 return (name, data)
649
650 def _getUnlabelEvent(self, label):
651 name = 'pull_request'
652 data = {
653 'action': 'unlabeled',
654 'pull_request': {
655 'number': self.number,
656 'updated_at': self.updated_at,
657 'base': {
658 'ref': self.branch,
659 'repo': {
660 'full_name': self.project
661 }
662 },
663 'head': {
664 'sha': self.head_sha
665 }
666 },
667 'label': {
668 'name': label
669 }
670 }
671 return (name, data)
672
Gregory Haynes4fc12542015-04-22 20:38:06 -0700673 def _getRepo(self):
674 repo_path = os.path.join(self.upstream_root, self.project)
675 return git.Repo(repo_path)
676
677 def _createPRRef(self):
678 repo = self._getRepo()
679 GithubChangeReference.create(
680 repo, self._getPRReference(), 'refs/tags/init')
681
682 def _addCommitToRepo(self, reset=False):
683 repo = self._getRepo()
684 ref = repo.references[self._getPRReference()]
685 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100686 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700687 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100688 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700689 repo.head.reference = ref
690 zuul.merger.merger.reset_repo_to_head(repo)
691 repo.git.clean('-x', '-f', '-d')
692
693 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
Jan Hruban37615e52015-11-19 14:30:49 +0100694 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700695 fn = os.path.join(repo.working_dir, fn)
696 f = open(fn, 'w')
697 with open(fn, 'w') as f:
698 f.write("test %s %s\n" %
699 (self.branch, self.number))
700 repo.index.add([fn])
701
702 self.head_sha = repo.index.commit(msg).hexsha
703 repo.head.reference = 'master'
704 zuul.merger.merger.reset_repo_to_head(repo)
705 repo.git.clean('-x', '-f', '-d')
706 repo.heads['master'].checkout()
707
708 def _updateTimeStamp(self):
709 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
710
711 def getPRHeadSha(self):
712 repo = self._getRepo()
713 return repo.references[self._getPRReference()].commit.hexsha
714
Jan Hrubane252a732017-01-03 15:03:09 +0100715 def setStatus(self, state, url, description, context):
716 self.statuses[context] = {
717 'state': state,
718 'url': url,
719 'description': description
720 }
721
722 def _clearStatuses(self):
723 self.statuses = {}
724
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 def _getPRReference(self):
726 return '%s/head' % self.number
727
728 def _getPullRequestEvent(self, action):
729 name = 'pull_request'
730 data = {
731 'action': action,
732 'number': self.number,
733 'pull_request': {
734 'number': self.number,
735 'updated_at': self.updated_at,
736 'base': {
737 'ref': self.branch,
738 'repo': {
739 'full_name': self.project
740 }
741 },
742 'head': {
743 'sha': self.head_sha
744 }
745 }
746 }
747 return (name, data)
748
749
750class FakeGithubConnection(githubconnection.GithubConnection):
751 log = logging.getLogger("zuul.test.FakeGithubConnection")
752
753 def __init__(self, driver, connection_name, connection_config,
754 upstream_root=None):
755 super(FakeGithubConnection, self).__init__(driver, connection_name,
756 connection_config)
757 self.connection_name = connection_name
758 self.pr_number = 0
759 self.pull_requests = []
760 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100761 self.merge_failure = False
762 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700763
Jan Hruban37615e52015-11-19 14:30:49 +0100764 def openFakePullRequest(self, project, branch, subject):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700765 self.pr_number += 1
766 pull_request = FakeGithubPullRequest(
Jan Hruban37615e52015-11-19 14:30:49 +0100767 self, self.pr_number, project, branch, subject, self.upstream_root)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700768 self.pull_requests.append(pull_request)
769 return pull_request
770
Wayne1a78c612015-06-11 17:14:13 -0700771 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
772 if not old_rev:
773 old_rev = '00000000000000000000000000000000'
774 if not new_rev:
775 new_rev = random_sha1()
776 name = 'push'
777 data = {
778 'ref': ref,
779 'before': old_rev,
780 'after': new_rev,
781 'repository': {
782 'full_name': project
783 }
784 }
785 return (name, data)
786
Gregory Haynes4fc12542015-04-22 20:38:06 -0700787 def emitEvent(self, event):
788 """Emulates sending the GitHub webhook event to the connection."""
789 port = self.webapp.server.socket.getsockname()[1]
790 name, data = event
791 payload = json.dumps(data)
792 headers = {'X-Github-Event': name}
793 req = urllib.request.Request(
794 'http://localhost:%s/connection/%s/payload'
795 % (port, self.connection_name),
796 data=payload, headers=headers)
797 urllib.request.urlopen(req)
798
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200799 def getPull(self, project, number):
800 pr = self.pull_requests[number - 1]
801 data = {
802 'number': number,
803 'updated_at': pr.updated_at,
804 'base': {
805 'repo': {
806 'full_name': pr.project
807 },
808 'ref': pr.branch,
809 },
Jan Hruban37615e52015-11-19 14:30:49 +0100810 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200811 'head': {
812 'sha': pr.head_sha
813 }
814 }
815 return data
816
Gregory Haynes4fc12542015-04-22 20:38:06 -0700817 def getGitUrl(self, project):
818 return os.path.join(self.upstream_root, str(project))
819
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200820 def real_getGitUrl(self, project):
821 return super(FakeGithubConnection, self).getGitUrl(project)
822
Gregory Haynes4fc12542015-04-22 20:38:06 -0700823 def getProjectBranches(self, project):
824 """Masks getProjectBranches since we don't have a real github"""
825
826 # just returns master for now
827 return ['master']
828
Jan Hrubane252a732017-01-03 15:03:09 +0100829 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700830 pull_request = self.pull_requests[pr_number - 1]
831 pull_request.addComment(message)
832
Jan Hruban49bff072015-11-03 11:45:46 +0100833 def mergePull(self, project, pr_number, sha=None):
834 pull_request = self.pull_requests[pr_number - 1]
835 if self.merge_failure:
836 raise Exception('Pull request was not merged')
837 if self.merge_not_allowed_count > 0:
838 self.merge_not_allowed_count -= 1
839 raise MergeFailure('Merge was not successful due to mergeability'
840 ' conflict')
841 pull_request.is_merged = True
842
Jan Hrubane252a732017-01-03 15:03:09 +0100843 def setCommitStatus(self, project, sha, state,
844 url='', description='', context=''):
845 owner, proj = project.split('/')
846 for pr in self.pull_requests:
847 pr_owner, pr_project = pr.project.split('/')
848 if (pr_owner == owner and pr_project == proj and
849 pr.head_sha == sha):
850 pr.setStatus(state, url, description, context)
851
Jan Hruban16ad31f2015-11-07 14:39:07 +0100852 def labelPull(self, project, pr_number, label):
853 pull_request = self.pull_requests[pr_number - 1]
854 pull_request.addLabel(label)
855
856 def unlabelPull(self, project, pr_number, label):
857 pull_request = self.pull_requests[pr_number - 1]
858 pull_request.removeLabel(label)
859
Gregory Haynes4fc12542015-04-22 20:38:06 -0700860
Clark Boylanb640e052014-04-03 16:41:46 -0700861class BuildHistory(object):
862 def __init__(self, **kw):
863 self.__dict__.update(kw)
864
865 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700866 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
867 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700868
869
870class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200871 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700872 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700873 self.url = url
874
875 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700876 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700877 path = res.path
878 project = '/'.join(path.split('/')[2:-2])
879 ret = '001e# service=git-upload-pack\n'
880 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
881 'multi_ack thin-pack side-band side-band-64k ofs-delta '
882 'shallow no-progress include-tag multi_ack_detailed no-done\n')
883 path = os.path.join(self.upstream_root, project)
884 repo = git.Repo(path)
885 for ref in repo.refs:
886 r = ref.object.hexsha + ' ' + ref.path + '\n'
887 ret += '%04x%s' % (len(r) + 4, r)
888 ret += '0000'
889 return ret
890
891
Clark Boylanb640e052014-04-03 16:41:46 -0700892class FakeStatsd(threading.Thread):
893 def __init__(self):
894 threading.Thread.__init__(self)
895 self.daemon = True
896 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
897 self.sock.bind(('', 0))
898 self.port = self.sock.getsockname()[1]
899 self.wake_read, self.wake_write = os.pipe()
900 self.stats = []
901
902 def run(self):
903 while True:
904 poll = select.poll()
905 poll.register(self.sock, select.POLLIN)
906 poll.register(self.wake_read, select.POLLIN)
907 ret = poll.poll()
908 for (fd, event) in ret:
909 if fd == self.sock.fileno():
910 data = self.sock.recvfrom(1024)
911 if not data:
912 return
913 self.stats.append(data[0])
914 if fd == self.wake_read:
915 return
916
917 def stop(self):
918 os.write(self.wake_write, '1\n')
919
920
James E. Blaire1767bc2016-08-02 10:00:27 -0700921class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700922 log = logging.getLogger("zuul.test")
923
Paul Belanger174a8272017-03-14 13:20:10 -0400924 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700925 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400926 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700927 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700928 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700929 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700930 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700931 # TODOv3(jeblair): self.node is really "the image of the node
932 # assigned". We should rename it (self.node_image?) if we
933 # keep using it like this, or we may end up exposing more of
934 # the complexity around multi-node jobs here
935 # (self.nodes[0].image?)
936 self.node = None
937 if len(self.parameters.get('nodes')) == 1:
938 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700939 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100940 self.pipeline = self.parameters['ZUUL_PIPELINE']
941 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700942 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700943 self.wait_condition = threading.Condition()
944 self.waiting = False
945 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500946 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700947 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700948 self.changes = None
949 if 'ZUUL_CHANGE_IDS' in self.parameters:
950 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700951
James E. Blair3158e282016-08-19 09:34:11 -0700952 def __repr__(self):
953 waiting = ''
954 if self.waiting:
955 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100956 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
957 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700958
Clark Boylanb640e052014-04-03 16:41:46 -0700959 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700960 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700961 self.wait_condition.acquire()
962 self.wait_condition.notify()
963 self.waiting = False
964 self.log.debug("Build %s released" % self.unique)
965 self.wait_condition.release()
966
967 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700968 """Return whether this build is being held.
969
970 :returns: Whether the build is being held.
971 :rtype: bool
972 """
973
Clark Boylanb640e052014-04-03 16:41:46 -0700974 self.wait_condition.acquire()
975 if self.waiting:
976 ret = True
977 else:
978 ret = False
979 self.wait_condition.release()
980 return ret
981
982 def _wait(self):
983 self.wait_condition.acquire()
984 self.waiting = True
985 self.log.debug("Build %s waiting" % self.unique)
986 self.wait_condition.wait()
987 self.wait_condition.release()
988
989 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -0700990 self.log.debug('Running build %s' % self.unique)
991
Paul Belanger174a8272017-03-14 13:20:10 -0400992 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.log.debug('Holding build %s' % self.unique)
994 self._wait()
995 self.log.debug("Build %s continuing" % self.unique)
996
James E. Blair412fba82017-01-26 15:00:50 -0800997 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -0700998 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -0800999 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001000 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001001 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001002 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001003 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001004
James E. Blaire1767bc2016-08-02 10:00:27 -07001005 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001006
James E. Blaira5dba232016-08-08 15:53:24 -07001007 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001008 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001009 for change in changes:
1010 if self.hasChanges(change):
1011 return True
1012 return False
1013
James E. Blaire7b99a02016-08-05 14:27:34 -07001014 def hasChanges(self, *changes):
1015 """Return whether this build has certain changes in its git repos.
1016
1017 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001018 are expected to be present (in order) in the git repository of
1019 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001020
1021 :returns: Whether the build has the indicated changes.
1022 :rtype: bool
1023
1024 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001025 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001026 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001027 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001028 try:
1029 repo = git.Repo(path)
1030 except NoSuchPathError as e:
1031 self.log.debug('%s' % e)
1032 return False
1033 ref = self.parameters['ZUUL_REF']
1034 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1035 commit_message = '%s-1' % change.subject
1036 self.log.debug("Checking if build %s has changes; commit_message "
1037 "%s; repo_messages %s" % (self, commit_message,
1038 repo_messages))
1039 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001040 self.log.debug(" messages do not match")
1041 return False
1042 self.log.debug(" OK")
1043 return True
1044
Clark Boylanb640e052014-04-03 16:41:46 -07001045
Paul Belanger174a8272017-03-14 13:20:10 -04001046class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1047 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001048
Paul Belanger174a8272017-03-14 13:20:10 -04001049 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001050 they will report that they have started but then pause until
1051 released before reporting completion. This attribute may be
1052 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001053 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001054 be explicitly released.
1055
1056 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001057 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001058 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001059 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001060 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001061 self.hold_jobs_in_build = False
1062 self.lock = threading.Lock()
1063 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001064 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001065 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001066 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001067
James E. Blaira5dba232016-08-08 15:53:24 -07001068 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001069 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001070
1071 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001072 :arg Change change: The :py:class:`~tests.base.FakeChange`
1073 instance which should cause the job to fail. This job
1074 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001075
1076 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001077 l = self.fail_tests.get(name, [])
1078 l.append(change)
1079 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001080
James E. Blair962220f2016-08-03 11:22:38 -07001081 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001082 """Release a held build.
1083
1084 :arg str regex: A regular expression which, if supplied, will
1085 cause only builds with matching names to be released. If
1086 not supplied, all builds will be released.
1087
1088 """
James E. Blair962220f2016-08-03 11:22:38 -07001089 builds = self.running_builds[:]
1090 self.log.debug("Releasing build %s (%s)" % (regex,
1091 len(self.running_builds)))
1092 for build in builds:
1093 if not regex or re.match(regex, build.name):
1094 self.log.debug("Releasing build %s" %
1095 (build.parameters['ZUUL_UUID']))
1096 build.release()
1097 else:
1098 self.log.debug("Not releasing build %s" %
1099 (build.parameters['ZUUL_UUID']))
1100 self.log.debug("Done releasing builds %s (%s)" %
1101 (regex, len(self.running_builds)))
1102
Paul Belanger174a8272017-03-14 13:20:10 -04001103 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001104 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001105 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001106 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001107 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001108 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001109 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001110 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001111 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1112 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001113
1114 def stopJob(self, job):
1115 self.log.debug("handle stop")
1116 parameters = json.loads(job.arguments)
1117 uuid = parameters['uuid']
1118 for build in self.running_builds:
1119 if build.unique == uuid:
1120 build.aborted = True
1121 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001122 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001123
James E. Blaira002b032017-04-18 10:35:48 -07001124 def stop(self):
1125 for build in self.running_builds:
1126 build.release()
1127 super(RecordingExecutorServer, self).stop()
1128
Joshua Hesketh50c21782016-10-13 21:34:14 +11001129
Paul Belanger174a8272017-03-14 13:20:10 -04001130class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001131 def doMergeChanges(self, items):
1132 # Get a merger in order to update the repos involved in this job.
1133 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1134 if not commit: # merge conflict
1135 self.recordResult('MERGER_FAILURE')
1136 return commit
1137
1138 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001139 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001140 self.executor_server.lock.acquire()
1141 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001142 BuildHistory(name=build.name, result=result, changes=build.changes,
1143 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001144 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001145 pipeline=build.parameters['ZUUL_PIPELINE'])
1146 )
Paul Belanger174a8272017-03-14 13:20:10 -04001147 self.executor_server.running_builds.remove(build)
1148 del self.executor_server.job_builds[self.job.unique]
1149 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001150
1151 def runPlaybooks(self, args):
1152 build = self.executor_server.job_builds[self.job.unique]
1153 build.jobdir = self.jobdir
1154
1155 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1156 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001157 return result
1158
Monty Taylore6562aa2017-02-20 07:37:39 -05001159 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001160 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001161
Paul Belanger174a8272017-03-14 13:20:10 -04001162 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001163 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001164 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001165 else:
1166 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001167 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001168
James E. Blairad8dca02017-02-21 11:48:32 -05001169 def getHostList(self, args):
1170 self.log.debug("hostlist")
1171 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001172 for host in hosts:
1173 host['host_vars']['ansible_connection'] = 'local'
1174
1175 hosts.append(dict(
1176 name='localhost',
1177 host_vars=dict(ansible_connection='local'),
1178 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001179 return hosts
1180
James E. Blairf5dbd002015-12-23 15:26:17 -08001181
Clark Boylanb640e052014-04-03 16:41:46 -07001182class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001183 """A Gearman server for use in tests.
1184
1185 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1186 added to the queue but will not be distributed to workers
1187 until released. This attribute may be changed at any time and
1188 will take effect for subsequently enqueued jobs, but
1189 previously held jobs will still need to be explicitly
1190 released.
1191
1192 """
1193
Clark Boylanb640e052014-04-03 16:41:46 -07001194 def __init__(self):
1195 self.hold_jobs_in_queue = False
1196 super(FakeGearmanServer, self).__init__(0)
1197
1198 def getJobForConnection(self, connection, peek=False):
1199 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1200 for job in queue:
1201 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001202 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001203 job.waiting = self.hold_jobs_in_queue
1204 else:
1205 job.waiting = False
1206 if job.waiting:
1207 continue
1208 if job.name in connection.functions:
1209 if not peek:
1210 queue.remove(job)
1211 connection.related_jobs[job.handle] = job
1212 job.worker_connection = connection
1213 job.running = True
1214 return job
1215 return None
1216
1217 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001218 """Release a held job.
1219
1220 :arg str regex: A regular expression which, if supplied, will
1221 cause only jobs with matching names to be released. If
1222 not supplied, all jobs will be released.
1223 """
Clark Boylanb640e052014-04-03 16:41:46 -07001224 released = False
1225 qlen = (len(self.high_queue) + len(self.normal_queue) +
1226 len(self.low_queue))
1227 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1228 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001229 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001230 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001231 parameters = json.loads(job.arguments)
1232 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001233 self.log.debug("releasing queued job %s" %
1234 job.unique)
1235 job.waiting = False
1236 released = True
1237 else:
1238 self.log.debug("not releasing queued job %s" %
1239 job.unique)
1240 if released:
1241 self.wakeConnections()
1242 qlen = (len(self.high_queue) + len(self.normal_queue) +
1243 len(self.low_queue))
1244 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1245
1246
1247class FakeSMTP(object):
1248 log = logging.getLogger('zuul.FakeSMTP')
1249
1250 def __init__(self, messages, server, port):
1251 self.server = server
1252 self.port = port
1253 self.messages = messages
1254
1255 def sendmail(self, from_email, to_email, msg):
1256 self.log.info("Sending email from %s, to %s, with msg %s" % (
1257 from_email, to_email, msg))
1258
1259 headers = msg.split('\n\n', 1)[0]
1260 body = msg.split('\n\n', 1)[1]
1261
1262 self.messages.append(dict(
1263 from_email=from_email,
1264 to_email=to_email,
1265 msg=msg,
1266 headers=headers,
1267 body=body,
1268 ))
1269
1270 return True
1271
1272 def quit(self):
1273 return True
1274
1275
James E. Blairdce6cea2016-12-20 16:45:32 -08001276class FakeNodepool(object):
1277 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001278 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001279
1280 log = logging.getLogger("zuul.test.FakeNodepool")
1281
1282 def __init__(self, host, port, chroot):
1283 self.client = kazoo.client.KazooClient(
1284 hosts='%s:%s%s' % (host, port, chroot))
1285 self.client.start()
1286 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001287 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001288 self.thread = threading.Thread(target=self.run)
1289 self.thread.daemon = True
1290 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001291 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001292
1293 def stop(self):
1294 self._running = False
1295 self.thread.join()
1296 self.client.stop()
1297 self.client.close()
1298
1299 def run(self):
1300 while self._running:
1301 self._run()
1302 time.sleep(0.1)
1303
1304 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001305 if self.paused:
1306 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001307 for req in self.getNodeRequests():
1308 self.fulfillRequest(req)
1309
1310 def getNodeRequests(self):
1311 try:
1312 reqids = self.client.get_children(self.REQUEST_ROOT)
1313 except kazoo.exceptions.NoNodeError:
1314 return []
1315 reqs = []
1316 for oid in sorted(reqids):
1317 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001318 try:
1319 data, stat = self.client.get(path)
1320 data = json.loads(data)
1321 data['_oid'] = oid
1322 reqs.append(data)
1323 except kazoo.exceptions.NoNodeError:
1324 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001325 return reqs
1326
James E. Blaire18d4602017-01-05 11:17:28 -08001327 def getNodes(self):
1328 try:
1329 nodeids = self.client.get_children(self.NODE_ROOT)
1330 except kazoo.exceptions.NoNodeError:
1331 return []
1332 nodes = []
1333 for oid in sorted(nodeids):
1334 path = self.NODE_ROOT + '/' + oid
1335 data, stat = self.client.get(path)
1336 data = json.loads(data)
1337 data['_oid'] = oid
1338 try:
1339 lockfiles = self.client.get_children(path + '/lock')
1340 except kazoo.exceptions.NoNodeError:
1341 lockfiles = []
1342 if lockfiles:
1343 data['_lock'] = True
1344 else:
1345 data['_lock'] = False
1346 nodes.append(data)
1347 return nodes
1348
James E. Blaira38c28e2017-01-04 10:33:20 -08001349 def makeNode(self, request_id, node_type):
1350 now = time.time()
1351 path = '/nodepool/nodes/'
1352 data = dict(type=node_type,
1353 provider='test-provider',
1354 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001355 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001356 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001357 public_ipv4='127.0.0.1',
1358 private_ipv4=None,
1359 public_ipv6=None,
1360 allocated_to=request_id,
1361 state='ready',
1362 state_time=now,
1363 created_time=now,
1364 updated_time=now,
1365 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001366 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001367 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001368 data = json.dumps(data)
1369 path = self.client.create(path, data,
1370 makepath=True,
1371 sequence=True)
1372 nodeid = path.split("/")[-1]
1373 return nodeid
1374
James E. Blair6ab79e02017-01-06 10:10:17 -08001375 def addFailRequest(self, request):
1376 self.fail_requests.add(request['_oid'])
1377
James E. Blairdce6cea2016-12-20 16:45:32 -08001378 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001379 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001380 return
1381 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001382 oid = request['_oid']
1383 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001384
James E. Blair6ab79e02017-01-06 10:10:17 -08001385 if oid in self.fail_requests:
1386 request['state'] = 'failed'
1387 else:
1388 request['state'] = 'fulfilled'
1389 nodes = []
1390 for node in request['node_types']:
1391 nodeid = self.makeNode(oid, node)
1392 nodes.append(nodeid)
1393 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001394
James E. Blaira38c28e2017-01-04 10:33:20 -08001395 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001396 path = self.REQUEST_ROOT + '/' + oid
1397 data = json.dumps(request)
1398 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1399 self.client.set(path, data)
1400
1401
James E. Blair498059b2016-12-20 13:50:13 -08001402class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001403 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001404 super(ChrootedKazooFixture, self).__init__()
1405
1406 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1407 if ':' in zk_host:
1408 host, port = zk_host.split(':')
1409 else:
1410 host = zk_host
1411 port = None
1412
1413 self.zookeeper_host = host
1414
1415 if not port:
1416 self.zookeeper_port = 2181
1417 else:
1418 self.zookeeper_port = int(port)
1419
Clark Boylan621ec9a2017-04-07 17:41:33 -07001420 self.test_id = test_id
1421
James E. Blair498059b2016-12-20 13:50:13 -08001422 def _setUp(self):
1423 # Make sure the test chroot paths do not conflict
1424 random_bits = ''.join(random.choice(string.ascii_lowercase +
1425 string.ascii_uppercase)
1426 for x in range(8))
1427
Clark Boylan621ec9a2017-04-07 17:41:33 -07001428 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001429 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1430
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001431 self.addCleanup(self._cleanup)
1432
James E. Blair498059b2016-12-20 13:50:13 -08001433 # Ensure the chroot path exists and clean up any pre-existing znodes.
1434 _tmp_client = kazoo.client.KazooClient(
1435 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1436 _tmp_client.start()
1437
1438 if _tmp_client.exists(self.zookeeper_chroot):
1439 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1440
1441 _tmp_client.ensure_path(self.zookeeper_chroot)
1442 _tmp_client.stop()
1443 _tmp_client.close()
1444
James E. Blair498059b2016-12-20 13:50:13 -08001445 def _cleanup(self):
1446 '''Remove the chroot path.'''
1447 # Need a non-chroot'ed client to remove the chroot path
1448 _tmp_client = kazoo.client.KazooClient(
1449 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1450 _tmp_client.start()
1451 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1452 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001453 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001454
1455
Joshua Heskethd78b4482015-09-14 16:56:34 -06001456class MySQLSchemaFixture(fixtures.Fixture):
1457 def setUp(self):
1458 super(MySQLSchemaFixture, self).setUp()
1459
1460 random_bits = ''.join(random.choice(string.ascii_lowercase +
1461 string.ascii_uppercase)
1462 for x in range(8))
1463 self.name = '%s_%s' % (random_bits, os.getpid())
1464 self.passwd = uuid.uuid4().hex
1465 db = pymysql.connect(host="localhost",
1466 user="openstack_citest",
1467 passwd="openstack_citest",
1468 db="openstack_citest")
1469 cur = db.cursor()
1470 cur.execute("create database %s" % self.name)
1471 cur.execute(
1472 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1473 (self.name, self.name, self.passwd))
1474 cur.execute("flush privileges")
1475
1476 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1477 self.passwd,
1478 self.name)
1479 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1480 self.addCleanup(self.cleanup)
1481
1482 def cleanup(self):
1483 db = pymysql.connect(host="localhost",
1484 user="openstack_citest",
1485 passwd="openstack_citest",
1486 db="openstack_citest")
1487 cur = db.cursor()
1488 cur.execute("drop database %s" % self.name)
1489 cur.execute("drop user '%s'@'localhost'" % self.name)
1490 cur.execute("flush privileges")
1491
1492
Maru Newby3fe5f852015-01-13 04:22:14 +00001493class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001494 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001495 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001496
James E. Blair1c236df2017-02-01 14:07:24 -08001497 def attachLogs(self, *args):
1498 def reader():
1499 self._log_stream.seek(0)
1500 while True:
1501 x = self._log_stream.read(4096)
1502 if not x:
1503 break
1504 yield x.encode('utf8')
1505 content = testtools.content.content_from_reader(
1506 reader,
1507 testtools.content_type.UTF8_TEXT,
1508 False)
1509 self.addDetail('logging', content)
1510
Clark Boylanb640e052014-04-03 16:41:46 -07001511 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001512 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001513 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1514 try:
1515 test_timeout = int(test_timeout)
1516 except ValueError:
1517 # If timeout value is invalid do not set a timeout.
1518 test_timeout = 0
1519 if test_timeout > 0:
1520 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1521
1522 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1523 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1524 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1525 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1526 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1527 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1528 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1529 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1530 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1531 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001532 self._log_stream = StringIO()
1533 self.addOnException(self.attachLogs)
1534 else:
1535 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001536
James E. Blair1c236df2017-02-01 14:07:24 -08001537 handler = logging.StreamHandler(self._log_stream)
1538 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1539 '%(levelname)-8s %(message)s')
1540 handler.setFormatter(formatter)
1541
1542 logger = logging.getLogger()
1543 logger.setLevel(logging.DEBUG)
1544 logger.addHandler(handler)
1545
Clark Boylan3410d532017-04-25 12:35:29 -07001546 # Make sure we don't carry old handlers around in process state
1547 # which slows down test runs
1548 self.addCleanup(logger.removeHandler, handler)
1549 self.addCleanup(handler.close)
1550 self.addCleanup(handler.flush)
1551
James E. Blair1c236df2017-02-01 14:07:24 -08001552 # NOTE(notmorgan): Extract logging overrides for specific
1553 # libraries from the OS_LOG_DEFAULTS env and create loggers
1554 # for each. This is used to limit the output during test runs
1555 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001556 log_defaults_from_env = os.environ.get(
1557 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001558 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001559
James E. Blairdce6cea2016-12-20 16:45:32 -08001560 if log_defaults_from_env:
1561 for default in log_defaults_from_env.split(','):
1562 try:
1563 name, level_str = default.split('=', 1)
1564 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001565 logger = logging.getLogger(name)
1566 logger.setLevel(level)
1567 logger.addHandler(handler)
1568 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001569 except ValueError:
1570 # NOTE(notmorgan): Invalid format of the log default,
1571 # skip and don't try and apply a logger for the
1572 # specified module
1573 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001574
Maru Newby3fe5f852015-01-13 04:22:14 +00001575
1576class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001577 """A test case with a functioning Zuul.
1578
1579 The following class variables are used during test setup and can
1580 be overidden by subclasses but are effectively read-only once a
1581 test method starts running:
1582
1583 :cvar str config_file: This points to the main zuul config file
1584 within the fixtures directory. Subclasses may override this
1585 to obtain a different behavior.
1586
1587 :cvar str tenant_config_file: This is the tenant config file
1588 (which specifies from what git repos the configuration should
1589 be loaded). It defaults to the value specified in
1590 `config_file` but can be overidden by subclasses to obtain a
1591 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001592 configuration. See also the :py:func:`simple_layout`
1593 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001594
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001595 :cvar bool create_project_keys: Indicates whether Zuul should
1596 auto-generate keys for each project, or whether the test
1597 infrastructure should insert dummy keys to save time during
1598 startup. Defaults to False.
1599
James E. Blaire7b99a02016-08-05 14:27:34 -07001600 The following are instance variables that are useful within test
1601 methods:
1602
1603 :ivar FakeGerritConnection fake_<connection>:
1604 A :py:class:`~tests.base.FakeGerritConnection` will be
1605 instantiated for each connection present in the config file
1606 and stored here. For instance, `fake_gerrit` will hold the
1607 FakeGerritConnection object for a connection named `gerrit`.
1608
1609 :ivar FakeGearmanServer gearman_server: An instance of
1610 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1611 server that all of the Zuul components in this test use to
1612 communicate with each other.
1613
Paul Belanger174a8272017-03-14 13:20:10 -04001614 :ivar RecordingExecutorServer executor_server: An instance of
1615 :py:class:`~tests.base.RecordingExecutorServer` which is the
1616 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001617
1618 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1619 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001620 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001621 list upon completion.
1622
1623 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1624 objects representing completed builds. They are appended to
1625 the list in the order they complete.
1626
1627 """
1628
James E. Blair83005782015-12-11 14:46:03 -08001629 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001630 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001631 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001632
1633 def _startMerger(self):
1634 self.merge_server = zuul.merger.server.MergeServer(self.config,
1635 self.connections)
1636 self.merge_server.start()
1637
Maru Newby3fe5f852015-01-13 04:22:14 +00001638 def setUp(self):
1639 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001640
1641 self.setupZK()
1642
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001643 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001644 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001645 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1646 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001647 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001648 tmp_root = tempfile.mkdtemp(
1649 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001650 self.test_root = os.path.join(tmp_root, "zuul-test")
1651 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001652 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001653 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001654 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001655
1656 if os.path.exists(self.test_root):
1657 shutil.rmtree(self.test_root)
1658 os.makedirs(self.test_root)
1659 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001660 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001661
1662 # Make per test copy of Configuration.
1663 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001664 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001665 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001666 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001667 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001668 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001669 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001670
Clark Boylanb640e052014-04-03 16:41:46 -07001671 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001672 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1673 # see: https://github.com/jsocol/pystatsd/issues/61
1674 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001675 os.environ['STATSD_PORT'] = str(self.statsd.port)
1676 self.statsd.start()
1677 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001678 reload_module(statsd)
1679 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001680
1681 self.gearman_server = FakeGearmanServer()
1682
1683 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001684 self.log.info("Gearman server on port %s" %
1685 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001686
James E. Blaire511d2f2016-12-08 15:22:26 -08001687 gerritsource.GerritSource.replication_timeout = 1.5
1688 gerritsource.GerritSource.replication_retry_interval = 0.5
1689 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001690
Joshua Hesketh352264b2015-08-11 23:42:08 +10001691 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001692
Jan Hruban7083edd2015-08-21 14:00:54 +02001693 self.webapp = zuul.webapp.WebApp(
1694 self.sched, port=0, listen_address='127.0.0.1')
1695
Jan Hruban6b71aff2015-10-22 16:58:08 +02001696 self.event_queues = [
1697 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001698 self.sched.trigger_event_queue,
1699 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001700 ]
1701
James E. Blairfef78942016-03-11 16:28:56 -08001702 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001703 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001704
Clark Boylanb640e052014-04-03 16:41:46 -07001705 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001706 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001707 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001708 return FakeURLOpener(self.upstream_root, *args, **kw)
1709
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001710 old_urlopen = urllib.request.urlopen
1711 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001712
James E. Blair3f876d52016-07-22 13:07:14 -07001713 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001714
Paul Belanger174a8272017-03-14 13:20:10 -04001715 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001716 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001717 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001718 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001719 _test_root=self.test_root,
1720 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001721 self.executor_server.start()
1722 self.history = self.executor_server.build_history
1723 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001724
Paul Belanger174a8272017-03-14 13:20:10 -04001725 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001726 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001727 self.merge_client = zuul.merger.client.MergeClient(
1728 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001729 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001730 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001731 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001732
James E. Blair0d5a36e2017-02-21 10:53:44 -05001733 self.fake_nodepool = FakeNodepool(
1734 self.zk_chroot_fixture.zookeeper_host,
1735 self.zk_chroot_fixture.zookeeper_port,
1736 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001737
Paul Belanger174a8272017-03-14 13:20:10 -04001738 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001739 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001740 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001741 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001742
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001743 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001744
1745 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001746 self.webapp.start()
1747 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001748 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001749 # Cleanups are run in reverse order
1750 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001751 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001752 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001753
James E. Blairb9c0d772017-03-03 14:34:49 -08001754 self.sched.reconfigure(self.config)
1755 self.sched.resume()
1756
James E. Blairfef78942016-03-11 16:28:56 -08001757 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001758 # Set up gerrit related fakes
1759 # Set a changes database so multiple FakeGerrit's can report back to
1760 # a virtual canonical database given by the configured hostname
1761 self.gerrit_changes_dbs = {}
1762
1763 def getGerritConnection(driver, name, config):
1764 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1765 con = FakeGerritConnection(driver, name, config,
1766 changes_db=db,
1767 upstream_root=self.upstream_root)
1768 self.event_queues.append(con.event_queue)
1769 setattr(self, 'fake_' + name, con)
1770 return con
1771
1772 self.useFixture(fixtures.MonkeyPatch(
1773 'zuul.driver.gerrit.GerritDriver.getConnection',
1774 getGerritConnection))
1775
Gregory Haynes4fc12542015-04-22 20:38:06 -07001776 def getGithubConnection(driver, name, config):
1777 con = FakeGithubConnection(driver, name, config,
1778 upstream_root=self.upstream_root)
1779 setattr(self, 'fake_' + name, con)
1780 return con
1781
1782 self.useFixture(fixtures.MonkeyPatch(
1783 'zuul.driver.github.GithubDriver.getConnection',
1784 getGithubConnection))
1785
James E. Blaire511d2f2016-12-08 15:22:26 -08001786 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001787 # TODO(jhesketh): This should come from lib.connections for better
1788 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001789 # Register connections from the config
1790 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001791
Joshua Hesketh352264b2015-08-11 23:42:08 +10001792 def FakeSMTPFactory(*args, **kw):
1793 args = [self.smtp_messages] + list(args)
1794 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001795
Joshua Hesketh352264b2015-08-11 23:42:08 +10001796 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001797
James E. Blaire511d2f2016-12-08 15:22:26 -08001798 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001799 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001800 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001801
James E. Blair83005782015-12-11 14:46:03 -08001802 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001803 # This creates the per-test configuration object. It can be
1804 # overriden by subclasses, but should not need to be since it
1805 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001806 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001807 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001808
1809 if not self.setupSimpleLayout():
1810 if hasattr(self, 'tenant_config_file'):
1811 self.config.set('zuul', 'tenant_config',
1812 self.tenant_config_file)
1813 git_path = os.path.join(
1814 os.path.dirname(
1815 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1816 'git')
1817 if os.path.exists(git_path):
1818 for reponame in os.listdir(git_path):
1819 project = reponame.replace('_', '/')
1820 self.copyDirToRepo(project,
1821 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001822 self.setupAllProjectKeys()
1823
James E. Blair06cc3922017-04-19 10:08:10 -07001824 def setupSimpleLayout(self):
1825 # If the test method has been decorated with a simple_layout,
1826 # use that instead of the class tenant_config_file. Set up a
1827 # single config-project with the specified layout, and
1828 # initialize repos for all of the 'project' entries which
1829 # appear in the layout.
1830 test_name = self.id().split('.')[-1]
1831 test = getattr(self, test_name)
1832 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001833 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001834 else:
1835 return False
1836
James E. Blairb70e55a2017-04-19 12:57:02 -07001837 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001838 path = os.path.join(FIXTURE_DIR, path)
1839 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001840 data = f.read()
1841 layout = yaml.safe_load(data)
1842 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001843 untrusted_projects = []
1844 for item in layout:
1845 if 'project' in item:
1846 name = item['project']['name']
1847 untrusted_projects.append(name)
1848 self.init_repo(name)
1849 self.addCommitToRepo(name, 'initial commit',
1850 files={'README': ''},
1851 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001852 if 'job' in item:
1853 jobname = item['job']['name']
1854 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001855
1856 root = os.path.join(self.test_root, "config")
1857 if not os.path.exists(root):
1858 os.makedirs(root)
1859 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1860 config = [{'tenant':
1861 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001862 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001863 {'config-projects': ['common-config'],
1864 'untrusted-projects': untrusted_projects}}}}]
1865 f.write(yaml.dump(config))
1866 f.close()
1867 self.config.set('zuul', 'tenant_config',
1868 os.path.join(FIXTURE_DIR, f.name))
1869
1870 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001871 self.addCommitToRepo('common-config', 'add content from fixture',
1872 files, branch='master', tag='init')
1873
1874 return True
1875
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001876 def setupAllProjectKeys(self):
1877 if self.create_project_keys:
1878 return
1879
1880 path = self.config.get('zuul', 'tenant_config')
1881 with open(os.path.join(FIXTURE_DIR, path)) as f:
1882 tenant_config = yaml.safe_load(f.read())
1883 for tenant in tenant_config:
1884 sources = tenant['tenant']['source']
1885 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001886 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001887 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001888 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001889 self.setupProjectKeys(source, project)
1890
1891 def setupProjectKeys(self, source, project):
1892 # Make sure we set up an RSA key for the project so that we
1893 # don't spend time generating one:
1894
1895 key_root = os.path.join(self.state_root, 'keys')
1896 if not os.path.isdir(key_root):
1897 os.mkdir(key_root, 0o700)
1898 private_key_file = os.path.join(key_root, source, project + '.pem')
1899 private_key_dir = os.path.dirname(private_key_file)
1900 self.log.debug("Installing test keys for project %s at %s" % (
1901 project, private_key_file))
1902 if not os.path.isdir(private_key_dir):
1903 os.makedirs(private_key_dir)
1904 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1905 with open(private_key_file, 'w') as o:
1906 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001907
James E. Blair498059b2016-12-20 13:50:13 -08001908 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001909 self.zk_chroot_fixture = self.useFixture(
1910 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001911 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001912 self.zk_chroot_fixture.zookeeper_host,
1913 self.zk_chroot_fixture.zookeeper_port,
1914 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001915
James E. Blair96c6bf82016-01-15 16:20:40 -08001916 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001917 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001918
1919 files = {}
1920 for (dirpath, dirnames, filenames) in os.walk(source_path):
1921 for filename in filenames:
1922 test_tree_filepath = os.path.join(dirpath, filename)
1923 common_path = os.path.commonprefix([test_tree_filepath,
1924 source_path])
1925 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1926 with open(test_tree_filepath, 'r') as f:
1927 content = f.read()
1928 files[relative_filepath] = content
1929 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001930 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001931
James E. Blaire18d4602017-01-05 11:17:28 -08001932 def assertNodepoolState(self):
1933 # Make sure that there are no pending requests
1934
1935 requests = self.fake_nodepool.getNodeRequests()
1936 self.assertEqual(len(requests), 0)
1937
1938 nodes = self.fake_nodepool.getNodes()
1939 for node in nodes:
1940 self.assertFalse(node['_lock'], "Node %s is locked" %
1941 (node['_oid'],))
1942
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001943 def assertNoGeneratedKeys(self):
1944 # Make sure that Zuul did not generate any project keys
1945 # (unless it was supposed to).
1946
1947 if self.create_project_keys:
1948 return
1949
1950 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1951 test_key = i.read()
1952
1953 key_root = os.path.join(self.state_root, 'keys')
1954 for root, dirname, files in os.walk(key_root):
1955 for fn in files:
1956 with open(os.path.join(root, fn)) as f:
1957 self.assertEqual(test_key, f.read())
1958
Clark Boylanb640e052014-04-03 16:41:46 -07001959 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001960 self.log.debug("Assert final state")
1961 # Make sure no jobs are running
1962 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001963 # Make sure that git.Repo objects have been garbage collected.
1964 repos = []
1965 gc.collect()
1966 for obj in gc.get_objects():
1967 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08001968 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07001969 repos.append(obj)
1970 self.assertEqual(len(repos), 0)
1971 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08001972 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001973 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08001974 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08001975 for tenant in self.sched.abide.tenants.values():
1976 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08001977 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08001978 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07001979
1980 def shutdown(self):
1981 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04001982 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07001983 self.merge_server.stop()
1984 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07001985 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04001986 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07001987 self.sched.stop()
1988 self.sched.join()
1989 self.statsd.stop()
1990 self.statsd.join()
1991 self.webapp.stop()
1992 self.webapp.join()
1993 self.rpc.stop()
1994 self.rpc.join()
1995 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08001996 self.fake_nodepool.stop()
1997 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07001998 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07001999 # we whitelist watchdog threads as they have relatively long delays
2000 # before noticing they should exit, but they should exit on their own.
2001 threads = [t for t in threading.enumerate()
2002 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002003 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002004 log_str = ""
2005 for thread_id, stack_frame in sys._current_frames().items():
2006 log_str += "Thread: %s\n" % thread_id
2007 log_str += "".join(traceback.format_stack(stack_frame))
2008 self.log.debug(log_str)
2009 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002010
James E. Blaira002b032017-04-18 10:35:48 -07002011 def assertCleanShutdown(self):
2012 pass
2013
James E. Blairc4ba97a2017-04-19 16:26:24 -07002014 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002015 parts = project.split('/')
2016 path = os.path.join(self.upstream_root, *parts[:-1])
2017 if not os.path.exists(path):
2018 os.makedirs(path)
2019 path = os.path.join(self.upstream_root, project)
2020 repo = git.Repo.init(path)
2021
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002022 with repo.config_writer() as config_writer:
2023 config_writer.set_value('user', 'email', 'user@example.com')
2024 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002025
Clark Boylanb640e052014-04-03 16:41:46 -07002026 repo.index.commit('initial commit')
2027 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002028 if tag:
2029 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002030
James E. Blair97d902e2014-08-21 13:25:56 -07002031 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002032 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002033 repo.git.clean('-x', '-f', '-d')
2034
James E. Blair97d902e2014-08-21 13:25:56 -07002035 def create_branch(self, project, branch):
2036 path = os.path.join(self.upstream_root, project)
2037 repo = git.Repo.init(path)
2038 fn = os.path.join(path, 'README')
2039
2040 branch_head = repo.create_head(branch)
2041 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002042 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002043 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002044 f.close()
2045 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002046 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002047
James E. Blair97d902e2014-08-21 13:25:56 -07002048 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002049 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002050 repo.git.clean('-x', '-f', '-d')
2051
Sachi King9f16d522016-03-16 12:20:45 +11002052 def create_commit(self, project):
2053 path = os.path.join(self.upstream_root, project)
2054 repo = git.Repo(path)
2055 repo.head.reference = repo.heads['master']
2056 file_name = os.path.join(path, 'README')
2057 with open(file_name, 'a') as f:
2058 f.write('creating fake commit\n')
2059 repo.index.add([file_name])
2060 commit = repo.index.commit('Creating a fake commit')
2061 return commit.hexsha
2062
James E. Blairf4a5f022017-04-18 14:01:10 -07002063 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002064 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002065 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002066 while len(self.builds):
2067 self.release(self.builds[0])
2068 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002069 i += 1
2070 if count is not None and i >= count:
2071 break
James E. Blairb8c16472015-05-05 14:55:26 -07002072
Clark Boylanb640e052014-04-03 16:41:46 -07002073 def release(self, job):
2074 if isinstance(job, FakeBuild):
2075 job.release()
2076 else:
2077 job.waiting = False
2078 self.log.debug("Queued job %s released" % job.unique)
2079 self.gearman_server.wakeConnections()
2080
2081 def getParameter(self, job, name):
2082 if isinstance(job, FakeBuild):
2083 return job.parameters[name]
2084 else:
2085 parameters = json.loads(job.arguments)
2086 return parameters[name]
2087
Clark Boylanb640e052014-04-03 16:41:46 -07002088 def haveAllBuildsReported(self):
2089 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002090 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002091 return False
2092 # Find out if every build that the worker has completed has been
2093 # reported back to Zuul. If it hasn't then that means a Gearman
2094 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002095 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002096 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002097 if not zbuild:
2098 # It has already been reported
2099 continue
2100 # It hasn't been reported yet.
2101 return False
2102 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002103 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002104 if connection.state == 'GRAB_WAIT':
2105 return False
2106 return True
2107
2108 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002109 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002110 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002111 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002112 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002113 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002114 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002115 for j in conn.related_jobs.values():
2116 if j.unique == build.uuid:
2117 client_job = j
2118 break
2119 if not client_job:
2120 self.log.debug("%s is not known to the gearman client" %
2121 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002122 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002123 if not client_job.handle:
2124 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002125 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002126 server_job = self.gearman_server.jobs.get(client_job.handle)
2127 if not server_job:
2128 self.log.debug("%s is not known to the gearman server" %
2129 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002130 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002131 if not hasattr(server_job, 'waiting'):
2132 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002133 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002134 if server_job.waiting:
2135 continue
James E. Blair17302972016-08-10 16:11:42 -07002136 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002137 self.log.debug("%s has not reported start" % build)
2138 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002139 worker_build = self.executor_server.job_builds.get(
2140 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002141 if worker_build:
2142 if worker_build.isWaiting():
2143 continue
2144 else:
2145 self.log.debug("%s is running" % worker_build)
2146 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002147 else:
James E. Blair962220f2016-08-03 11:22:38 -07002148 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002149 return False
James E. Blaira002b032017-04-18 10:35:48 -07002150 for (build_uuid, job_worker) in \
2151 self.executor_server.job_workers.items():
2152 if build_uuid not in seen_builds:
2153 self.log.debug("%s is not finalized" % build_uuid)
2154 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002155 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002156
James E. Blairdce6cea2016-12-20 16:45:32 -08002157 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002158 if self.fake_nodepool.paused:
2159 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002160 if self.sched.nodepool.requests:
2161 return False
2162 return True
2163
Jan Hruban6b71aff2015-10-22 16:58:08 +02002164 def eventQueuesEmpty(self):
2165 for queue in self.event_queues:
2166 yield queue.empty()
2167
2168 def eventQueuesJoin(self):
2169 for queue in self.event_queues:
2170 queue.join()
2171
Clark Boylanb640e052014-04-03 16:41:46 -07002172 def waitUntilSettled(self):
2173 self.log.debug("Waiting until settled...")
2174 start = time.time()
2175 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002176 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002177 self.log.error("Timeout waiting for Zuul to settle")
2178 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002179 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002180 self.log.error(" %s: %s" % (queue, queue.empty()))
2181 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002182 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002183 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002184 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002185 self.log.error("All requests completed: %s" %
2186 (self.areAllNodeRequestsComplete(),))
2187 self.log.error("Merge client jobs: %s" %
2188 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002189 raise Exception("Timeout waiting for Zuul to settle")
2190 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002191
Paul Belanger174a8272017-03-14 13:20:10 -04002192 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002193 # have all build states propogated to zuul?
2194 if self.haveAllBuildsReported():
2195 # Join ensures that the queue is empty _and_ events have been
2196 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002197 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002198 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002199 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002200 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002201 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002202 self.areAllNodeRequestsComplete() and
2203 all(self.eventQueuesEmpty())):
2204 # The queue empty check is placed at the end to
2205 # ensure that if a component adds an event between
2206 # when locked the run handler and checked that the
2207 # components were stable, we don't erroneously
2208 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002209 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002210 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002211 self.log.debug("...settled.")
2212 return
2213 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002214 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002215 self.sched.wake_event.wait(0.1)
2216
2217 def countJobResults(self, jobs, result):
2218 jobs = filter(lambda x: x.result == result, jobs)
2219 return len(jobs)
2220
James E. Blair96c6bf82016-01-15 16:20:40 -08002221 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002222 for job in self.history:
2223 if (job.name == name and
2224 (project is None or
2225 job.parameters['ZUUL_PROJECT'] == project)):
2226 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002227 raise Exception("Unable to find job %s in history" % name)
2228
2229 def assertEmptyQueues(self):
2230 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002231 for tenant in self.sched.abide.tenants.values():
2232 for pipeline in tenant.layout.pipelines.values():
2233 for queue in pipeline.queues:
2234 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002235 print('pipeline %s queue %s contents %s' % (
2236 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002237 self.assertEqual(len(queue.queue), 0,
2238 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002239
2240 def assertReportedStat(self, key, value=None, kind=None):
2241 start = time.time()
2242 while time.time() < (start + 5):
2243 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002244 k, v = stat.split(':')
2245 if key == k:
2246 if value is None and kind is None:
2247 return
2248 elif value:
2249 if value == v:
2250 return
2251 elif kind:
2252 if v.endswith('|' + kind):
2253 return
2254 time.sleep(0.1)
2255
Clark Boylanb640e052014-04-03 16:41:46 -07002256 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002257
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002258 def assertBuilds(self, builds):
2259 """Assert that the running builds are as described.
2260
2261 The list of running builds is examined and must match exactly
2262 the list of builds described by the input.
2263
2264 :arg list builds: A list of dictionaries. Each item in the
2265 list must match the corresponding build in the build
2266 history, and each element of the dictionary must match the
2267 corresponding attribute of the build.
2268
2269 """
James E. Blair3158e282016-08-19 09:34:11 -07002270 try:
2271 self.assertEqual(len(self.builds), len(builds))
2272 for i, d in enumerate(builds):
2273 for k, v in d.items():
2274 self.assertEqual(
2275 getattr(self.builds[i], k), v,
2276 "Element %i in builds does not match" % (i,))
2277 except Exception:
2278 for build in self.builds:
2279 self.log.error("Running build: %s" % build)
2280 else:
2281 self.log.error("No running builds")
2282 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002283
James E. Blairb536ecc2016-08-31 10:11:42 -07002284 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002285 """Assert that the completed builds are as described.
2286
2287 The list of completed builds is examined and must match
2288 exactly the list of builds described by the input.
2289
2290 :arg list history: A list of dictionaries. Each item in the
2291 list must match the corresponding build in the build
2292 history, and each element of the dictionary must match the
2293 corresponding attribute of the build.
2294
James E. Blairb536ecc2016-08-31 10:11:42 -07002295 :arg bool ordered: If true, the history must match the order
2296 supplied, if false, the builds are permitted to have
2297 arrived in any order.
2298
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002299 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002300 def matches(history_item, item):
2301 for k, v in item.items():
2302 if getattr(history_item, k) != v:
2303 return False
2304 return True
James E. Blair3158e282016-08-19 09:34:11 -07002305 try:
2306 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002307 if ordered:
2308 for i, d in enumerate(history):
2309 if not matches(self.history[i], d):
2310 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002311 "Element %i in history does not match %s" %
2312 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002313 else:
2314 unseen = self.history[:]
2315 for i, d in enumerate(history):
2316 found = False
2317 for unseen_item in unseen:
2318 if matches(unseen_item, d):
2319 found = True
2320 unseen.remove(unseen_item)
2321 break
2322 if not found:
2323 raise Exception("No match found for element %i "
2324 "in history" % (i,))
2325 if unseen:
2326 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002327 except Exception:
2328 for build in self.history:
2329 self.log.error("Completed build: %s" % build)
2330 else:
2331 self.log.error("No completed builds")
2332 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002333
James E. Blair6ac368c2016-12-22 18:07:20 -08002334 def printHistory(self):
2335 """Log the build history.
2336
2337 This can be useful during tests to summarize what jobs have
2338 completed.
2339
2340 """
2341 self.log.debug("Build history:")
2342 for build in self.history:
2343 self.log.debug(build)
2344
James E. Blair59fdbac2015-12-07 17:08:06 -08002345 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002346 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2347
James E. Blair9ea70072017-04-19 16:05:30 -07002348 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002349 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002350 if not os.path.exists(root):
2351 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002352 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2353 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002354- tenant:
2355 name: openstack
2356 source:
2357 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002358 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002359 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002360 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002361 - org/project
2362 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002363 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002364 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002365 self.config.set('zuul', 'tenant_config',
2366 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002367 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002368
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002369 def addCommitToRepo(self, project, message, files,
2370 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002371 path = os.path.join(self.upstream_root, project)
2372 repo = git.Repo(path)
2373 repo.head.reference = branch
2374 zuul.merger.merger.reset_repo_to_head(repo)
2375 for fn, content in files.items():
2376 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002377 try:
2378 os.makedirs(os.path.dirname(fn))
2379 except OSError:
2380 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002381 with open(fn, 'w') as f:
2382 f.write(content)
2383 repo.index.add([fn])
2384 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002385 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002386 repo.heads[branch].commit = commit
2387 repo.head.reference = branch
2388 repo.git.clean('-x', '-f', '-d')
2389 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002390 if tag:
2391 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002392 return before
2393
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002394 def commitConfigUpdate(self, project_name, source_name):
2395 """Commit an update to zuul.yaml
2396
2397 This overwrites the zuul.yaml in the specificed project with
2398 the contents specified.
2399
2400 :arg str project_name: The name of the project containing
2401 zuul.yaml (e.g., common-config)
2402
2403 :arg str source_name: The path to the file (underneath the
2404 test fixture directory) whose contents should be used to
2405 replace zuul.yaml.
2406 """
2407
2408 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002409 files = {}
2410 with open(source_path, 'r') as f:
2411 data = f.read()
2412 layout = yaml.safe_load(data)
2413 files['zuul.yaml'] = data
2414 for item in layout:
2415 if 'job' in item:
2416 jobname = item['job']['name']
2417 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002418 before = self.addCommitToRepo(
2419 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002420 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002421 return before
2422
James E. Blair7fc8daa2016-08-08 15:37:15 -07002423 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002424
James E. Blair7fc8daa2016-08-08 15:37:15 -07002425 """Inject a Fake (Gerrit) event.
2426
2427 This method accepts a JSON-encoded event and simulates Zuul
2428 having received it from Gerrit. It could (and should)
2429 eventually apply to any connection type, but is currently only
2430 used with Gerrit connections. The name of the connection is
2431 used to look up the corresponding server, and the event is
2432 simulated as having been received by all Zuul connections
2433 attached to that server. So if two Gerrit connections in Zuul
2434 are connected to the same Gerrit server, and you invoke this
2435 method specifying the name of one of them, the event will be
2436 received by both.
2437
2438 .. note::
2439
2440 "self.fake_gerrit.addEvent" calls should be migrated to
2441 this method.
2442
2443 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002444 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002445 :arg str event: The JSON-encoded event.
2446
2447 """
2448 specified_conn = self.connections.connections[connection]
2449 for conn in self.connections.connections.values():
2450 if (isinstance(conn, specified_conn.__class__) and
2451 specified_conn.server == conn.server):
2452 conn.addEvent(event)
2453
James E. Blair3f876d52016-07-22 13:07:14 -07002454
2455class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002456 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002457 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002458
Joshua Heskethd78b4482015-09-14 16:56:34 -06002459
2460class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002461 def setup_config(self):
2462 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002463 for section_name in self.config.sections():
2464 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2465 section_name, re.I)
2466 if not con_match:
2467 continue
2468
2469 if self.config.get(section_name, 'driver') == 'sql':
2470 f = MySQLSchemaFixture()
2471 self.useFixture(f)
2472 if (self.config.get(section_name, 'dburi') ==
2473 '$MYSQL_FIXTURE_DBURI$'):
2474 self.config.set(section_name, 'dburi', f.dburi)