blob: 1757a7e8aff309f39b1bf39c6a343654feeb67c4 [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():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
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
Clark Boylanb640e052014-04-03 16:41:46 -0700499 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 if 'submit' in action:
502 change.setMerged()
503 if message:
504 change.setReported()
505
506 def query(self, number):
507 change = self.changes.get(int(number))
508 if change:
509 return change.query()
510 return {}
511
James E. Blairc494d542014-08-06 09:23:52 -0700512 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700513 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700514 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800515 if query.startswith('change:'):
516 # Query a specific changeid
517 changeid = query[len('change:'):]
518 l = [change.query() for change in self.changes.values()
519 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700520 elif query.startswith('message:'):
521 # Query the content of a commit message
522 msg = query[len('message:'):].strip()
523 l = [change.query() for change in self.changes.values()
524 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800525 else:
526 # Query all open changes
527 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700528 return l
James E. Blairc494d542014-08-06 09:23:52 -0700529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700531 pass
532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def getGitUrl(self, project):
534 return os.path.join(self.upstream_root, project.name)
535
Clark Boylanb640e052014-04-03 16:41:46 -0700536
Gregory Haynes4fc12542015-04-22 20:38:06 -0700537class GithubChangeReference(git.Reference):
538 _common_path_default = "refs/pull"
539 _points_to_commits_only = True
540
541
542class FakeGithubPullRequest(object):
543
544 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100545 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700546 """Creates a new PR with several commits.
547 Sends an event about opened PR."""
548 self.github = github
549 self.source = github
550 self.number = number
551 self.project = project
552 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100553 self.subject = subject
554 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700555 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100556 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100558 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100559 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.updated_at = None
561 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100562 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100563 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100565 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700566 self._updateTimeStamp()
567
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700573
Jan Hruban570d01c2016-03-10 21:51:32 +0100574 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700575 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100576 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100578 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579
580 def getPullRequestOpenedEvent(self):
581 return self._getPullRequestEvent('opened')
582
583 def getPullRequestSynchronizeEvent(self):
584 return self._getPullRequestEvent('synchronize')
585
586 def getPullRequestReopenedEvent(self):
587 return self._getPullRequestEvent('reopened')
588
589 def getPullRequestClosedEvent(self):
590 return self._getPullRequestEvent('closed')
591
592 def addComment(self, message):
593 self.comments.append(message)
594 self._updateTimeStamp()
595
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200596 def getCommentAddedEvent(self, text):
597 name = 'issue_comment'
598 data = {
599 'action': 'created',
600 'issue': {
601 'number': self.number
602 },
603 'comment': {
604 'body': text
605 },
606 'repository': {
607 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100608 },
609 'sender': {
610 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200611 }
612 }
613 return (name, data)
614
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800615 def getReviewAddedEvent(self, review):
616 name = 'pull_request_review'
617 data = {
618 'action': 'submitted',
619 'pull_request': {
620 'number': self.number,
621 'title': self.subject,
622 'updated_at': self.updated_at,
623 'base': {
624 'ref': self.branch,
625 'repo': {
626 'full_name': self.project
627 }
628 },
629 'head': {
630 'sha': self.head_sha
631 }
632 },
633 'review': {
634 'state': review
635 },
636 'repository': {
637 'full_name': self.project
638 },
639 'sender': {
640 'login': 'ghuser'
641 }
642 }
643 return (name, data)
644
Jan Hruban16ad31f2015-11-07 14:39:07 +0100645 def addLabel(self, name):
646 if name not in self.labels:
647 self.labels.append(name)
648 self._updateTimeStamp()
649 return self._getLabelEvent(name)
650
651 def removeLabel(self, name):
652 if name in self.labels:
653 self.labels.remove(name)
654 self._updateTimeStamp()
655 return self._getUnlabelEvent(name)
656
657 def _getLabelEvent(self, label):
658 name = 'pull_request'
659 data = {
660 'action': 'labeled',
661 'pull_request': {
662 'number': self.number,
663 'updated_at': self.updated_at,
664 'base': {
665 'ref': self.branch,
666 'repo': {
667 'full_name': self.project
668 }
669 },
670 'head': {
671 'sha': self.head_sha
672 }
673 },
674 'label': {
675 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100676 },
677 'sender': {
678 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100679 }
680 }
681 return (name, data)
682
683 def _getUnlabelEvent(self, label):
684 name = 'pull_request'
685 data = {
686 'action': 'unlabeled',
687 'pull_request': {
688 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100689 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100690 'updated_at': self.updated_at,
691 'base': {
692 'ref': self.branch,
693 'repo': {
694 'full_name': self.project
695 }
696 },
697 'head': {
698 'sha': self.head_sha
699 }
700 },
701 'label': {
702 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100703 },
704 'sender': {
705 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100706 }
707 }
708 return (name, data)
709
Gregory Haynes4fc12542015-04-22 20:38:06 -0700710 def _getRepo(self):
711 repo_path = os.path.join(self.upstream_root, self.project)
712 return git.Repo(repo_path)
713
714 def _createPRRef(self):
715 repo = self._getRepo()
716 GithubChangeReference.create(
717 repo, self._getPRReference(), 'refs/tags/init')
718
Jan Hruban570d01c2016-03-10 21:51:32 +0100719 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700720 repo = self._getRepo()
721 ref = repo.references[self._getPRReference()]
722 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100723 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700724 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100725 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 repo.head.reference = ref
727 zuul.merger.merger.reset_repo_to_head(repo)
728 repo.git.clean('-x', '-f', '-d')
729
Jan Hruban570d01c2016-03-10 21:51:32 +0100730 if files:
731 fn = files[0]
732 self.files = files
733 else:
734 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
735 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100736 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700737 fn = os.path.join(repo.working_dir, fn)
738 f = open(fn, 'w')
739 with open(fn, 'w') as f:
740 f.write("test %s %s\n" %
741 (self.branch, self.number))
742 repo.index.add([fn])
743
744 self.head_sha = repo.index.commit(msg).hexsha
745 repo.head.reference = 'master'
746 zuul.merger.merger.reset_repo_to_head(repo)
747 repo.git.clean('-x', '-f', '-d')
748 repo.heads['master'].checkout()
749
750 def _updateTimeStamp(self):
751 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
752
753 def getPRHeadSha(self):
754 repo = self._getRepo()
755 return repo.references[self._getPRReference()].commit.hexsha
756
Jan Hrubane252a732017-01-03 15:03:09 +0100757 def setStatus(self, state, url, description, context):
758 self.statuses[context] = {
759 'state': state,
760 'url': url,
761 'description': description
762 }
763
764 def _clearStatuses(self):
765 self.statuses = {}
766
Gregory Haynes4fc12542015-04-22 20:38:06 -0700767 def _getPRReference(self):
768 return '%s/head' % self.number
769
770 def _getPullRequestEvent(self, action):
771 name = 'pull_request'
772 data = {
773 'action': action,
774 'number': self.number,
775 'pull_request': {
776 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100777 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700778 'updated_at': self.updated_at,
779 'base': {
780 'ref': self.branch,
781 'repo': {
782 'full_name': self.project
783 }
784 },
785 'head': {
786 'sha': self.head_sha
787 }
Jan Hruban3b415922016-02-03 13:10:22 +0100788 },
789 'sender': {
790 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700791 }
792 }
793 return (name, data)
794
795
796class FakeGithubConnection(githubconnection.GithubConnection):
797 log = logging.getLogger("zuul.test.FakeGithubConnection")
798
799 def __init__(self, driver, connection_name, connection_config,
800 upstream_root=None):
801 super(FakeGithubConnection, self).__init__(driver, connection_name,
802 connection_config)
803 self.connection_name = connection_name
804 self.pr_number = 0
805 self.pull_requests = []
806 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100807 self.merge_failure = False
808 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700809
Jan Hruban570d01c2016-03-10 21:51:32 +0100810 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700811 self.pr_number += 1
812 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100813 self, self.pr_number, project, branch, subject, self.upstream_root,
814 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700815 self.pull_requests.append(pull_request)
816 return pull_request
817
Wayne1a78c612015-06-11 17:14:13 -0700818 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
819 if not old_rev:
820 old_rev = '00000000000000000000000000000000'
821 if not new_rev:
822 new_rev = random_sha1()
823 name = 'push'
824 data = {
825 'ref': ref,
826 'before': old_rev,
827 'after': new_rev,
828 'repository': {
829 'full_name': project
830 }
831 }
832 return (name, data)
833
Gregory Haynes4fc12542015-04-22 20:38:06 -0700834 def emitEvent(self, event):
835 """Emulates sending the GitHub webhook event to the connection."""
836 port = self.webapp.server.socket.getsockname()[1]
837 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700838 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 headers = {'X-Github-Event': name}
840 req = urllib.request.Request(
841 'http://localhost:%s/connection/%s/payload'
842 % (port, self.connection_name),
843 data=payload, headers=headers)
844 urllib.request.urlopen(req)
845
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200846 def getPull(self, project, number):
847 pr = self.pull_requests[number - 1]
848 data = {
849 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100850 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200851 'updated_at': pr.updated_at,
852 'base': {
853 'repo': {
854 'full_name': pr.project
855 },
856 'ref': pr.branch,
857 },
Jan Hruban37615e52015-11-19 14:30:49 +0100858 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200859 'head': {
860 'sha': pr.head_sha
861 }
862 }
863 return data
864
Jan Hruban570d01c2016-03-10 21:51:32 +0100865 def getPullFileNames(self, project, number):
866 pr = self.pull_requests[number - 1]
867 return pr.files
868
Jan Hruban3b415922016-02-03 13:10:22 +0100869 def getUser(self, login):
870 data = {
871 'username': login,
872 'name': 'Github User',
873 'email': 'github.user@example.com'
874 }
875 return data
876
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 def getGitUrl(self, project):
878 return os.path.join(self.upstream_root, str(project))
879
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200880 def real_getGitUrl(self, project):
881 return super(FakeGithubConnection, self).getGitUrl(project)
882
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 def getProjectBranches(self, project):
884 """Masks getProjectBranches since we don't have a real github"""
885
886 # just returns master for now
887 return ['master']
888
Jan Hrubane252a732017-01-03 15:03:09 +0100889 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700890 pull_request = self.pull_requests[pr_number - 1]
891 pull_request.addComment(message)
892
Jan Hruban3b415922016-02-03 13:10:22 +0100893 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100894 pull_request = self.pull_requests[pr_number - 1]
895 if self.merge_failure:
896 raise Exception('Pull request was not merged')
897 if self.merge_not_allowed_count > 0:
898 self.merge_not_allowed_count -= 1
899 raise MergeFailure('Merge was not successful due to mergeability'
900 ' conflict')
901 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100902 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100903
Jan Hrubane252a732017-01-03 15:03:09 +0100904 def setCommitStatus(self, project, sha, state,
905 url='', description='', context=''):
906 owner, proj = project.split('/')
907 for pr in self.pull_requests:
908 pr_owner, pr_project = pr.project.split('/')
909 if (pr_owner == owner and pr_project == proj and
910 pr.head_sha == sha):
911 pr.setStatus(state, url, description, context)
912
Jan Hruban16ad31f2015-11-07 14:39:07 +0100913 def labelPull(self, project, pr_number, label):
914 pull_request = self.pull_requests[pr_number - 1]
915 pull_request.addLabel(label)
916
917 def unlabelPull(self, project, pr_number, label):
918 pull_request = self.pull_requests[pr_number - 1]
919 pull_request.removeLabel(label)
920
Gregory Haynes4fc12542015-04-22 20:38:06 -0700921
Clark Boylanb640e052014-04-03 16:41:46 -0700922class BuildHistory(object):
923 def __init__(self, **kw):
924 self.__dict__.update(kw)
925
926 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700927 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
928 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700929
930
931class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200932 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700933 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700934 self.url = url
935
936 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700937 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700938 path = res.path
939 project = '/'.join(path.split('/')[2:-2])
940 ret = '001e# service=git-upload-pack\n'
941 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
942 'multi_ack thin-pack side-band side-band-64k ofs-delta '
943 'shallow no-progress include-tag multi_ack_detailed no-done\n')
944 path = os.path.join(self.upstream_root, project)
945 repo = git.Repo(path)
946 for ref in repo.refs:
947 r = ref.object.hexsha + ' ' + ref.path + '\n'
948 ret += '%04x%s' % (len(r) + 4, r)
949 ret += '0000'
950 return ret
951
952
Clark Boylanb640e052014-04-03 16:41:46 -0700953class FakeStatsd(threading.Thread):
954 def __init__(self):
955 threading.Thread.__init__(self)
956 self.daemon = True
957 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
958 self.sock.bind(('', 0))
959 self.port = self.sock.getsockname()[1]
960 self.wake_read, self.wake_write = os.pipe()
961 self.stats = []
962
963 def run(self):
964 while True:
965 poll = select.poll()
966 poll.register(self.sock, select.POLLIN)
967 poll.register(self.wake_read, select.POLLIN)
968 ret = poll.poll()
969 for (fd, event) in ret:
970 if fd == self.sock.fileno():
971 data = self.sock.recvfrom(1024)
972 if not data:
973 return
974 self.stats.append(data[0])
975 if fd == self.wake_read:
976 return
977
978 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -0700979 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -0700980
981
James E. Blaire1767bc2016-08-02 10:00:27 -0700982class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700983 log = logging.getLogger("zuul.test")
984
Paul Belanger174a8272017-03-14 13:20:10 -0400985 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700986 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400987 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700988 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700989 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700990 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700991 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700992 # TODOv3(jeblair): self.node is really "the image of the node
993 # assigned". We should rename it (self.node_image?) if we
994 # keep using it like this, or we may end up exposing more of
995 # the complexity around multi-node jobs here
996 # (self.nodes[0].image?)
997 self.node = None
998 if len(self.parameters.get('nodes')) == 1:
999 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001000 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001001 self.pipeline = self.parameters['ZUUL_PIPELINE']
1002 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001003 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001004 self.wait_condition = threading.Condition()
1005 self.waiting = False
1006 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001007 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001008 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001009 self.changes = None
1010 if 'ZUUL_CHANGE_IDS' in self.parameters:
1011 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001012
James E. Blair3158e282016-08-19 09:34:11 -07001013 def __repr__(self):
1014 waiting = ''
1015 if self.waiting:
1016 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001017 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1018 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001019
Clark Boylanb640e052014-04-03 16:41:46 -07001020 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001021 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001022 self.wait_condition.acquire()
1023 self.wait_condition.notify()
1024 self.waiting = False
1025 self.log.debug("Build %s released" % self.unique)
1026 self.wait_condition.release()
1027
1028 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001029 """Return whether this build is being held.
1030
1031 :returns: Whether the build is being held.
1032 :rtype: bool
1033 """
1034
Clark Boylanb640e052014-04-03 16:41:46 -07001035 self.wait_condition.acquire()
1036 if self.waiting:
1037 ret = True
1038 else:
1039 ret = False
1040 self.wait_condition.release()
1041 return ret
1042
1043 def _wait(self):
1044 self.wait_condition.acquire()
1045 self.waiting = True
1046 self.log.debug("Build %s waiting" % self.unique)
1047 self.wait_condition.wait()
1048 self.wait_condition.release()
1049
1050 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001051 self.log.debug('Running build %s' % self.unique)
1052
Paul Belanger174a8272017-03-14 13:20:10 -04001053 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001054 self.log.debug('Holding build %s' % self.unique)
1055 self._wait()
1056 self.log.debug("Build %s continuing" % self.unique)
1057
James E. Blair412fba82017-01-26 15:00:50 -08001058 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001059 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001060 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001061 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001062 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001063 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001064 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001065
James E. Blaire1767bc2016-08-02 10:00:27 -07001066 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001067
James E. Blaira5dba232016-08-08 15:53:24 -07001068 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001069 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001070 for change in changes:
1071 if self.hasChanges(change):
1072 return True
1073 return False
1074
James E. Blaire7b99a02016-08-05 14:27:34 -07001075 def hasChanges(self, *changes):
1076 """Return whether this build has certain changes in its git repos.
1077
1078 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001079 are expected to be present (in order) in the git repository of
1080 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001081
1082 :returns: Whether the build has the indicated changes.
1083 :rtype: bool
1084
1085 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001086 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001087 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001088 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001089 try:
1090 repo = git.Repo(path)
1091 except NoSuchPathError as e:
1092 self.log.debug('%s' % e)
1093 return False
1094 ref = self.parameters['ZUUL_REF']
1095 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1096 commit_message = '%s-1' % change.subject
1097 self.log.debug("Checking if build %s has changes; commit_message "
1098 "%s; repo_messages %s" % (self, commit_message,
1099 repo_messages))
1100 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001101 self.log.debug(" messages do not match")
1102 return False
1103 self.log.debug(" OK")
1104 return True
1105
Clark Boylanb640e052014-04-03 16:41:46 -07001106
Paul Belanger174a8272017-03-14 13:20:10 -04001107class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1108 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001109
Paul Belanger174a8272017-03-14 13:20:10 -04001110 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001111 they will report that they have started but then pause until
1112 released before reporting completion. This attribute may be
1113 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001114 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001115 be explicitly released.
1116
1117 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001118 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001119 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001120 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001121 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001122 self.hold_jobs_in_build = False
1123 self.lock = threading.Lock()
1124 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001125 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001126 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001127 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001128
James E. Blaira5dba232016-08-08 15:53:24 -07001129 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001130 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001131
1132 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001133 :arg Change change: The :py:class:`~tests.base.FakeChange`
1134 instance which should cause the job to fail. This job
1135 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001136
1137 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001138 l = self.fail_tests.get(name, [])
1139 l.append(change)
1140 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001141
James E. Blair962220f2016-08-03 11:22:38 -07001142 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001143 """Release a held build.
1144
1145 :arg str regex: A regular expression which, if supplied, will
1146 cause only builds with matching names to be released. If
1147 not supplied, all builds will be released.
1148
1149 """
James E. Blair962220f2016-08-03 11:22:38 -07001150 builds = self.running_builds[:]
1151 self.log.debug("Releasing build %s (%s)" % (regex,
1152 len(self.running_builds)))
1153 for build in builds:
1154 if not regex or re.match(regex, build.name):
1155 self.log.debug("Releasing build %s" %
1156 (build.parameters['ZUUL_UUID']))
1157 build.release()
1158 else:
1159 self.log.debug("Not releasing build %s" %
1160 (build.parameters['ZUUL_UUID']))
1161 self.log.debug("Done releasing builds %s (%s)" %
1162 (regex, len(self.running_builds)))
1163
Paul Belanger174a8272017-03-14 13:20:10 -04001164 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001165 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001166 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001167 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001168 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001169 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001170 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001171 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001172 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1173 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001174
1175 def stopJob(self, job):
1176 self.log.debug("handle stop")
1177 parameters = json.loads(job.arguments)
1178 uuid = parameters['uuid']
1179 for build in self.running_builds:
1180 if build.unique == uuid:
1181 build.aborted = True
1182 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001183 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001184
James E. Blaira002b032017-04-18 10:35:48 -07001185 def stop(self):
1186 for build in self.running_builds:
1187 build.release()
1188 super(RecordingExecutorServer, self).stop()
1189
Joshua Hesketh50c21782016-10-13 21:34:14 +11001190
Paul Belanger174a8272017-03-14 13:20:10 -04001191class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001192 def doMergeChanges(self, items):
1193 # Get a merger in order to update the repos involved in this job.
1194 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1195 if not commit: # merge conflict
1196 self.recordResult('MERGER_FAILURE')
1197 return commit
1198
1199 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001200 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001201 self.executor_server.lock.acquire()
1202 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001203 BuildHistory(name=build.name, result=result, changes=build.changes,
1204 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001205 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001206 pipeline=build.parameters['ZUUL_PIPELINE'])
1207 )
Paul Belanger174a8272017-03-14 13:20:10 -04001208 self.executor_server.running_builds.remove(build)
1209 del self.executor_server.job_builds[self.job.unique]
1210 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001211
1212 def runPlaybooks(self, args):
1213 build = self.executor_server.job_builds[self.job.unique]
1214 build.jobdir = self.jobdir
1215
1216 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1217 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001218 return result
1219
Monty Taylore6562aa2017-02-20 07:37:39 -05001220 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001221 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001222
Paul Belanger174a8272017-03-14 13:20:10 -04001223 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001224 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001225 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001226 else:
1227 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001228 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001229
James E. Blairad8dca02017-02-21 11:48:32 -05001230 def getHostList(self, args):
1231 self.log.debug("hostlist")
1232 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001233 for host in hosts:
1234 host['host_vars']['ansible_connection'] = 'local'
1235
1236 hosts.append(dict(
1237 name='localhost',
1238 host_vars=dict(ansible_connection='local'),
1239 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001240 return hosts
1241
James E. Blairf5dbd002015-12-23 15:26:17 -08001242
Clark Boylanb640e052014-04-03 16:41:46 -07001243class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001244 """A Gearman server for use in tests.
1245
1246 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1247 added to the queue but will not be distributed to workers
1248 until released. This attribute may be changed at any time and
1249 will take effect for subsequently enqueued jobs, but
1250 previously held jobs will still need to be explicitly
1251 released.
1252
1253 """
1254
Clark Boylanb640e052014-04-03 16:41:46 -07001255 def __init__(self):
1256 self.hold_jobs_in_queue = False
1257 super(FakeGearmanServer, self).__init__(0)
1258
1259 def getJobForConnection(self, connection, peek=False):
1260 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1261 for job in queue:
1262 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001263 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001264 job.waiting = self.hold_jobs_in_queue
1265 else:
1266 job.waiting = False
1267 if job.waiting:
1268 continue
1269 if job.name in connection.functions:
1270 if not peek:
1271 queue.remove(job)
1272 connection.related_jobs[job.handle] = job
1273 job.worker_connection = connection
1274 job.running = True
1275 return job
1276 return None
1277
1278 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001279 """Release a held job.
1280
1281 :arg str regex: A regular expression which, if supplied, will
1282 cause only jobs with matching names to be released. If
1283 not supplied, all jobs will be released.
1284 """
Clark Boylanb640e052014-04-03 16:41:46 -07001285 released = False
1286 qlen = (len(self.high_queue) + len(self.normal_queue) +
1287 len(self.low_queue))
1288 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1289 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001290 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001291 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001292 parameters = json.loads(job.arguments)
1293 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001294 self.log.debug("releasing queued job %s" %
1295 job.unique)
1296 job.waiting = False
1297 released = True
1298 else:
1299 self.log.debug("not releasing queued job %s" %
1300 job.unique)
1301 if released:
1302 self.wakeConnections()
1303 qlen = (len(self.high_queue) + len(self.normal_queue) +
1304 len(self.low_queue))
1305 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1306
1307
1308class FakeSMTP(object):
1309 log = logging.getLogger('zuul.FakeSMTP')
1310
1311 def __init__(self, messages, server, port):
1312 self.server = server
1313 self.port = port
1314 self.messages = messages
1315
1316 def sendmail(self, from_email, to_email, msg):
1317 self.log.info("Sending email from %s, to %s, with msg %s" % (
1318 from_email, to_email, msg))
1319
1320 headers = msg.split('\n\n', 1)[0]
1321 body = msg.split('\n\n', 1)[1]
1322
1323 self.messages.append(dict(
1324 from_email=from_email,
1325 to_email=to_email,
1326 msg=msg,
1327 headers=headers,
1328 body=body,
1329 ))
1330
1331 return True
1332
1333 def quit(self):
1334 return True
1335
1336
James E. Blairdce6cea2016-12-20 16:45:32 -08001337class FakeNodepool(object):
1338 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001339 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001340
1341 log = logging.getLogger("zuul.test.FakeNodepool")
1342
1343 def __init__(self, host, port, chroot):
1344 self.client = kazoo.client.KazooClient(
1345 hosts='%s:%s%s' % (host, port, chroot))
1346 self.client.start()
1347 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001348 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001349 self.thread = threading.Thread(target=self.run)
1350 self.thread.daemon = True
1351 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001352 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001353
1354 def stop(self):
1355 self._running = False
1356 self.thread.join()
1357 self.client.stop()
1358 self.client.close()
1359
1360 def run(self):
1361 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001362 try:
1363 self._run()
1364 except Exception:
1365 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001366 time.sleep(0.1)
1367
1368 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001369 if self.paused:
1370 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001371 for req in self.getNodeRequests():
1372 self.fulfillRequest(req)
1373
1374 def getNodeRequests(self):
1375 try:
1376 reqids = self.client.get_children(self.REQUEST_ROOT)
1377 except kazoo.exceptions.NoNodeError:
1378 return []
1379 reqs = []
1380 for oid in sorted(reqids):
1381 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001382 try:
1383 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001384 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001385 data['_oid'] = oid
1386 reqs.append(data)
1387 except kazoo.exceptions.NoNodeError:
1388 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001389 return reqs
1390
James E. Blaire18d4602017-01-05 11:17:28 -08001391 def getNodes(self):
1392 try:
1393 nodeids = self.client.get_children(self.NODE_ROOT)
1394 except kazoo.exceptions.NoNodeError:
1395 return []
1396 nodes = []
1397 for oid in sorted(nodeids):
1398 path = self.NODE_ROOT + '/' + oid
1399 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001400 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001401 data['_oid'] = oid
1402 try:
1403 lockfiles = self.client.get_children(path + '/lock')
1404 except kazoo.exceptions.NoNodeError:
1405 lockfiles = []
1406 if lockfiles:
1407 data['_lock'] = True
1408 else:
1409 data['_lock'] = False
1410 nodes.append(data)
1411 return nodes
1412
James E. Blaira38c28e2017-01-04 10:33:20 -08001413 def makeNode(self, request_id, node_type):
1414 now = time.time()
1415 path = '/nodepool/nodes/'
1416 data = dict(type=node_type,
1417 provider='test-provider',
1418 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001419 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001420 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001421 public_ipv4='127.0.0.1',
1422 private_ipv4=None,
1423 public_ipv6=None,
1424 allocated_to=request_id,
1425 state='ready',
1426 state_time=now,
1427 created_time=now,
1428 updated_time=now,
1429 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001430 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001431 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001432 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001433 path = self.client.create(path, data,
1434 makepath=True,
1435 sequence=True)
1436 nodeid = path.split("/")[-1]
1437 return nodeid
1438
James E. Blair6ab79e02017-01-06 10:10:17 -08001439 def addFailRequest(self, request):
1440 self.fail_requests.add(request['_oid'])
1441
James E. Blairdce6cea2016-12-20 16:45:32 -08001442 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001443 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001444 return
1445 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001446 oid = request['_oid']
1447 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001448
James E. Blair6ab79e02017-01-06 10:10:17 -08001449 if oid in self.fail_requests:
1450 request['state'] = 'failed'
1451 else:
1452 request['state'] = 'fulfilled'
1453 nodes = []
1454 for node in request['node_types']:
1455 nodeid = self.makeNode(oid, node)
1456 nodes.append(nodeid)
1457 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001458
James E. Blaira38c28e2017-01-04 10:33:20 -08001459 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001460 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001461 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001462 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001463 try:
1464 self.client.set(path, data)
1465 except kazoo.exceptions.NoNodeError:
1466 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001467
1468
James E. Blair498059b2016-12-20 13:50:13 -08001469class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001470 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001471 super(ChrootedKazooFixture, self).__init__()
1472
1473 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1474 if ':' in zk_host:
1475 host, port = zk_host.split(':')
1476 else:
1477 host = zk_host
1478 port = None
1479
1480 self.zookeeper_host = host
1481
1482 if not port:
1483 self.zookeeper_port = 2181
1484 else:
1485 self.zookeeper_port = int(port)
1486
Clark Boylan621ec9a2017-04-07 17:41:33 -07001487 self.test_id = test_id
1488
James E. Blair498059b2016-12-20 13:50:13 -08001489 def _setUp(self):
1490 # Make sure the test chroot paths do not conflict
1491 random_bits = ''.join(random.choice(string.ascii_lowercase +
1492 string.ascii_uppercase)
1493 for x in range(8))
1494
Clark Boylan621ec9a2017-04-07 17:41:33 -07001495 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001496 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1497
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001498 self.addCleanup(self._cleanup)
1499
James E. Blair498059b2016-12-20 13:50:13 -08001500 # Ensure the chroot path exists and clean up any pre-existing znodes.
1501 _tmp_client = kazoo.client.KazooClient(
1502 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1503 _tmp_client.start()
1504
1505 if _tmp_client.exists(self.zookeeper_chroot):
1506 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1507
1508 _tmp_client.ensure_path(self.zookeeper_chroot)
1509 _tmp_client.stop()
1510 _tmp_client.close()
1511
James E. Blair498059b2016-12-20 13:50:13 -08001512 def _cleanup(self):
1513 '''Remove the chroot path.'''
1514 # Need a non-chroot'ed client to remove the chroot path
1515 _tmp_client = kazoo.client.KazooClient(
1516 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1517 _tmp_client.start()
1518 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1519 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001520 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001521
1522
Joshua Heskethd78b4482015-09-14 16:56:34 -06001523class MySQLSchemaFixture(fixtures.Fixture):
1524 def setUp(self):
1525 super(MySQLSchemaFixture, self).setUp()
1526
1527 random_bits = ''.join(random.choice(string.ascii_lowercase +
1528 string.ascii_uppercase)
1529 for x in range(8))
1530 self.name = '%s_%s' % (random_bits, os.getpid())
1531 self.passwd = uuid.uuid4().hex
1532 db = pymysql.connect(host="localhost",
1533 user="openstack_citest",
1534 passwd="openstack_citest",
1535 db="openstack_citest")
1536 cur = db.cursor()
1537 cur.execute("create database %s" % self.name)
1538 cur.execute(
1539 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1540 (self.name, self.name, self.passwd))
1541 cur.execute("flush privileges")
1542
1543 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1544 self.passwd,
1545 self.name)
1546 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1547 self.addCleanup(self.cleanup)
1548
1549 def cleanup(self):
1550 db = pymysql.connect(host="localhost",
1551 user="openstack_citest",
1552 passwd="openstack_citest",
1553 db="openstack_citest")
1554 cur = db.cursor()
1555 cur.execute("drop database %s" % self.name)
1556 cur.execute("drop user '%s'@'localhost'" % self.name)
1557 cur.execute("flush privileges")
1558
1559
Maru Newby3fe5f852015-01-13 04:22:14 +00001560class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001561 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001562 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001563
James E. Blair1c236df2017-02-01 14:07:24 -08001564 def attachLogs(self, *args):
1565 def reader():
1566 self._log_stream.seek(0)
1567 while True:
1568 x = self._log_stream.read(4096)
1569 if not x:
1570 break
1571 yield x.encode('utf8')
1572 content = testtools.content.content_from_reader(
1573 reader,
1574 testtools.content_type.UTF8_TEXT,
1575 False)
1576 self.addDetail('logging', content)
1577
Clark Boylanb640e052014-04-03 16:41:46 -07001578 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001579 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001580 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1581 try:
1582 test_timeout = int(test_timeout)
1583 except ValueError:
1584 # If timeout value is invalid do not set a timeout.
1585 test_timeout = 0
1586 if test_timeout > 0:
1587 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1588
1589 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1590 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1591 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1592 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1593 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1594 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1595 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1596 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1597 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1598 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001599 self._log_stream = StringIO()
1600 self.addOnException(self.attachLogs)
1601 else:
1602 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001603
James E. Blair73b41772017-05-22 13:22:55 -07001604 # NOTE(jeblair): this is temporary extra debugging to try to
1605 # track down a possible leak.
1606 orig_git_repo_init = git.Repo.__init__
1607
1608 def git_repo_init(myself, *args, **kw):
1609 orig_git_repo_init(myself, *args, **kw)
1610 self.log.debug("Created git repo 0x%x %s" %
1611 (id(myself), repr(myself)))
1612
1613 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1614 git_repo_init))
1615
James E. Blair1c236df2017-02-01 14:07:24 -08001616 handler = logging.StreamHandler(self._log_stream)
1617 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1618 '%(levelname)-8s %(message)s')
1619 handler.setFormatter(formatter)
1620
1621 logger = logging.getLogger()
1622 logger.setLevel(logging.DEBUG)
1623 logger.addHandler(handler)
1624
Clark Boylan3410d532017-04-25 12:35:29 -07001625 # Make sure we don't carry old handlers around in process state
1626 # which slows down test runs
1627 self.addCleanup(logger.removeHandler, handler)
1628 self.addCleanup(handler.close)
1629 self.addCleanup(handler.flush)
1630
James E. Blair1c236df2017-02-01 14:07:24 -08001631 # NOTE(notmorgan): Extract logging overrides for specific
1632 # libraries from the OS_LOG_DEFAULTS env and create loggers
1633 # for each. This is used to limit the output during test runs
1634 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001635 log_defaults_from_env = os.environ.get(
1636 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001637 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001638
James E. Blairdce6cea2016-12-20 16:45:32 -08001639 if log_defaults_from_env:
1640 for default in log_defaults_from_env.split(','):
1641 try:
1642 name, level_str = default.split('=', 1)
1643 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001644 logger = logging.getLogger(name)
1645 logger.setLevel(level)
1646 logger.addHandler(handler)
1647 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001648 except ValueError:
1649 # NOTE(notmorgan): Invalid format of the log default,
1650 # skip and don't try and apply a logger for the
1651 # specified module
1652 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001653
Maru Newby3fe5f852015-01-13 04:22:14 +00001654
1655class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001656 """A test case with a functioning Zuul.
1657
1658 The following class variables are used during test setup and can
1659 be overidden by subclasses but are effectively read-only once a
1660 test method starts running:
1661
1662 :cvar str config_file: This points to the main zuul config file
1663 within the fixtures directory. Subclasses may override this
1664 to obtain a different behavior.
1665
1666 :cvar str tenant_config_file: This is the tenant config file
1667 (which specifies from what git repos the configuration should
1668 be loaded). It defaults to the value specified in
1669 `config_file` but can be overidden by subclasses to obtain a
1670 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001671 configuration. See also the :py:func:`simple_layout`
1672 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001673
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001674 :cvar bool create_project_keys: Indicates whether Zuul should
1675 auto-generate keys for each project, or whether the test
1676 infrastructure should insert dummy keys to save time during
1677 startup. Defaults to False.
1678
James E. Blaire7b99a02016-08-05 14:27:34 -07001679 The following are instance variables that are useful within test
1680 methods:
1681
1682 :ivar FakeGerritConnection fake_<connection>:
1683 A :py:class:`~tests.base.FakeGerritConnection` will be
1684 instantiated for each connection present in the config file
1685 and stored here. For instance, `fake_gerrit` will hold the
1686 FakeGerritConnection object for a connection named `gerrit`.
1687
1688 :ivar FakeGearmanServer gearman_server: An instance of
1689 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1690 server that all of the Zuul components in this test use to
1691 communicate with each other.
1692
Paul Belanger174a8272017-03-14 13:20:10 -04001693 :ivar RecordingExecutorServer executor_server: An instance of
1694 :py:class:`~tests.base.RecordingExecutorServer` which is the
1695 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001696
1697 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1698 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001699 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001700 list upon completion.
1701
1702 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1703 objects representing completed builds. They are appended to
1704 the list in the order they complete.
1705
1706 """
1707
James E. Blair83005782015-12-11 14:46:03 -08001708 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001709 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001710 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001711
1712 def _startMerger(self):
1713 self.merge_server = zuul.merger.server.MergeServer(self.config,
1714 self.connections)
1715 self.merge_server.start()
1716
Maru Newby3fe5f852015-01-13 04:22:14 +00001717 def setUp(self):
1718 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001719
1720 self.setupZK()
1721
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001722 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001723 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001724 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1725 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001726 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001727 tmp_root = tempfile.mkdtemp(
1728 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001729 self.test_root = os.path.join(tmp_root, "zuul-test")
1730 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001731 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001732 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001733 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001734
1735 if os.path.exists(self.test_root):
1736 shutil.rmtree(self.test_root)
1737 os.makedirs(self.test_root)
1738 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001739 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001740
1741 # Make per test copy of Configuration.
1742 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001743 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001744 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001745 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001746 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001747 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001748 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001749
Clark Boylanb640e052014-04-03 16:41:46 -07001750 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001751 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1752 # see: https://github.com/jsocol/pystatsd/issues/61
1753 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001754 os.environ['STATSD_PORT'] = str(self.statsd.port)
1755 self.statsd.start()
1756 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001757 reload_module(statsd)
1758 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001759
1760 self.gearman_server = FakeGearmanServer()
1761
1762 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001763 self.log.info("Gearman server on port %s" %
1764 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001765
James E. Blaire511d2f2016-12-08 15:22:26 -08001766 gerritsource.GerritSource.replication_timeout = 1.5
1767 gerritsource.GerritSource.replication_retry_interval = 0.5
1768 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001769
Joshua Hesketh352264b2015-08-11 23:42:08 +10001770 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001771
Jan Hruban7083edd2015-08-21 14:00:54 +02001772 self.webapp = zuul.webapp.WebApp(
1773 self.sched, port=0, listen_address='127.0.0.1')
1774
Jan Hruban6b71aff2015-10-22 16:58:08 +02001775 self.event_queues = [
1776 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001777 self.sched.trigger_event_queue,
1778 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001779 ]
1780
James E. Blairfef78942016-03-11 16:28:56 -08001781 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001782 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001783
Clark Boylanb640e052014-04-03 16:41:46 -07001784 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001785 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001786 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001787 return FakeURLOpener(self.upstream_root, *args, **kw)
1788
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001789 old_urlopen = urllib.request.urlopen
1790 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001791
Paul Belanger174a8272017-03-14 13:20:10 -04001792 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001793 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001794 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001795 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001796 _test_root=self.test_root,
1797 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001798 self.executor_server.start()
1799 self.history = self.executor_server.build_history
1800 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001801
Paul Belanger174a8272017-03-14 13:20:10 -04001802 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001803 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001804 self.merge_client = zuul.merger.client.MergeClient(
1805 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001806 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001808 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001809
James E. Blair0d5a36e2017-02-21 10:53:44 -05001810 self.fake_nodepool = FakeNodepool(
1811 self.zk_chroot_fixture.zookeeper_host,
1812 self.zk_chroot_fixture.zookeeper_port,
1813 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001814
Paul Belanger174a8272017-03-14 13:20:10 -04001815 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001816 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001817 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001818 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001819
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001820 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001821
1822 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001823 self.webapp.start()
1824 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001825 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001826 # Cleanups are run in reverse order
1827 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001828 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001829 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001830
James E. Blairb9c0d772017-03-03 14:34:49 -08001831 self.sched.reconfigure(self.config)
1832 self.sched.resume()
1833
James E. Blairfef78942016-03-11 16:28:56 -08001834 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001835 # Set up gerrit related fakes
1836 # Set a changes database so multiple FakeGerrit's can report back to
1837 # a virtual canonical database given by the configured hostname
1838 self.gerrit_changes_dbs = {}
1839
1840 def getGerritConnection(driver, name, config):
1841 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1842 con = FakeGerritConnection(driver, name, config,
1843 changes_db=db,
1844 upstream_root=self.upstream_root)
1845 self.event_queues.append(con.event_queue)
1846 setattr(self, 'fake_' + name, con)
1847 return con
1848
1849 self.useFixture(fixtures.MonkeyPatch(
1850 'zuul.driver.gerrit.GerritDriver.getConnection',
1851 getGerritConnection))
1852
Gregory Haynes4fc12542015-04-22 20:38:06 -07001853 def getGithubConnection(driver, name, config):
1854 con = FakeGithubConnection(driver, name, config,
1855 upstream_root=self.upstream_root)
1856 setattr(self, 'fake_' + name, con)
1857 return con
1858
1859 self.useFixture(fixtures.MonkeyPatch(
1860 'zuul.driver.github.GithubDriver.getConnection',
1861 getGithubConnection))
1862
James E. Blaire511d2f2016-12-08 15:22:26 -08001863 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001864 # TODO(jhesketh): This should come from lib.connections for better
1865 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001866 # Register connections from the config
1867 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001868
Joshua Hesketh352264b2015-08-11 23:42:08 +10001869 def FakeSMTPFactory(*args, **kw):
1870 args = [self.smtp_messages] + list(args)
1871 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001872
Joshua Hesketh352264b2015-08-11 23:42:08 +10001873 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001874
James E. Blaire511d2f2016-12-08 15:22:26 -08001875 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001876 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001877 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001878
James E. Blair83005782015-12-11 14:46:03 -08001879 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001880 # This creates the per-test configuration object. It can be
1881 # overriden by subclasses, but should not need to be since it
1882 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001883 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001884 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001885
1886 if not self.setupSimpleLayout():
1887 if hasattr(self, 'tenant_config_file'):
1888 self.config.set('zuul', 'tenant_config',
1889 self.tenant_config_file)
1890 git_path = os.path.join(
1891 os.path.dirname(
1892 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1893 'git')
1894 if os.path.exists(git_path):
1895 for reponame in os.listdir(git_path):
1896 project = reponame.replace('_', '/')
1897 self.copyDirToRepo(project,
1898 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001899 self.setupAllProjectKeys()
1900
James E. Blair06cc3922017-04-19 10:08:10 -07001901 def setupSimpleLayout(self):
1902 # If the test method has been decorated with a simple_layout,
1903 # use that instead of the class tenant_config_file. Set up a
1904 # single config-project with the specified layout, and
1905 # initialize repos for all of the 'project' entries which
1906 # appear in the layout.
1907 test_name = self.id().split('.')[-1]
1908 test = getattr(self, test_name)
1909 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001910 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001911 else:
1912 return False
1913
James E. Blairb70e55a2017-04-19 12:57:02 -07001914 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001915 path = os.path.join(FIXTURE_DIR, path)
1916 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001917 data = f.read()
1918 layout = yaml.safe_load(data)
1919 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001920 untrusted_projects = []
1921 for item in layout:
1922 if 'project' in item:
1923 name = item['project']['name']
1924 untrusted_projects.append(name)
1925 self.init_repo(name)
1926 self.addCommitToRepo(name, 'initial commit',
1927 files={'README': ''},
1928 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001929 if 'job' in item:
1930 jobname = item['job']['name']
1931 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001932
1933 root = os.path.join(self.test_root, "config")
1934 if not os.path.exists(root):
1935 os.makedirs(root)
1936 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1937 config = [{'tenant':
1938 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001939 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001940 {'config-projects': ['common-config'],
1941 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001942 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001943 f.close()
1944 self.config.set('zuul', 'tenant_config',
1945 os.path.join(FIXTURE_DIR, f.name))
1946
1947 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001948 self.addCommitToRepo('common-config', 'add content from fixture',
1949 files, branch='master', tag='init')
1950
1951 return True
1952
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001953 def setupAllProjectKeys(self):
1954 if self.create_project_keys:
1955 return
1956
1957 path = self.config.get('zuul', 'tenant_config')
1958 with open(os.path.join(FIXTURE_DIR, path)) as f:
1959 tenant_config = yaml.safe_load(f.read())
1960 for tenant in tenant_config:
1961 sources = tenant['tenant']['source']
1962 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001963 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001964 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001965 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001966 self.setupProjectKeys(source, project)
1967
1968 def setupProjectKeys(self, source, project):
1969 # Make sure we set up an RSA key for the project so that we
1970 # don't spend time generating one:
1971
1972 key_root = os.path.join(self.state_root, 'keys')
1973 if not os.path.isdir(key_root):
1974 os.mkdir(key_root, 0o700)
1975 private_key_file = os.path.join(key_root, source, project + '.pem')
1976 private_key_dir = os.path.dirname(private_key_file)
1977 self.log.debug("Installing test keys for project %s at %s" % (
1978 project, private_key_file))
1979 if not os.path.isdir(private_key_dir):
1980 os.makedirs(private_key_dir)
1981 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1982 with open(private_key_file, 'w') as o:
1983 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001984
James E. Blair498059b2016-12-20 13:50:13 -08001985 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001986 self.zk_chroot_fixture = self.useFixture(
1987 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001988 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001989 self.zk_chroot_fixture.zookeeper_host,
1990 self.zk_chroot_fixture.zookeeper_port,
1991 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001992
James E. Blair96c6bf82016-01-15 16:20:40 -08001993 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001994 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001995
1996 files = {}
1997 for (dirpath, dirnames, filenames) in os.walk(source_path):
1998 for filename in filenames:
1999 test_tree_filepath = os.path.join(dirpath, filename)
2000 common_path = os.path.commonprefix([test_tree_filepath,
2001 source_path])
2002 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2003 with open(test_tree_filepath, 'r') as f:
2004 content = f.read()
2005 files[relative_filepath] = content
2006 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002007 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002008
James E. Blaire18d4602017-01-05 11:17:28 -08002009 def assertNodepoolState(self):
2010 # Make sure that there are no pending requests
2011
2012 requests = self.fake_nodepool.getNodeRequests()
2013 self.assertEqual(len(requests), 0)
2014
2015 nodes = self.fake_nodepool.getNodes()
2016 for node in nodes:
2017 self.assertFalse(node['_lock'], "Node %s is locked" %
2018 (node['_oid'],))
2019
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002020 def assertNoGeneratedKeys(self):
2021 # Make sure that Zuul did not generate any project keys
2022 # (unless it was supposed to).
2023
2024 if self.create_project_keys:
2025 return
2026
2027 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2028 test_key = i.read()
2029
2030 key_root = os.path.join(self.state_root, 'keys')
2031 for root, dirname, files in os.walk(key_root):
2032 for fn in files:
2033 with open(os.path.join(root, fn)) as f:
2034 self.assertEqual(test_key, f.read())
2035
Clark Boylanb640e052014-04-03 16:41:46 -07002036 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002037 self.log.debug("Assert final state")
2038 # Make sure no jobs are running
2039 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002040 # Make sure that git.Repo objects have been garbage collected.
2041 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002042 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002043 gc.collect()
2044 for obj in gc.get_objects():
2045 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002046 self.log.debug("Leaked git repo object: 0x%x %s" %
2047 (id(obj), repr(obj)))
2048 for ref in gc.get_referrers(obj):
2049 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002050 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002051 if repos:
2052 for obj in gc.garbage:
2053 self.log.debug(" Garbage %s" % (repr(obj)))
2054 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002055 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002056 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002057 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002058 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002059 for tenant in self.sched.abide.tenants.values():
2060 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002061 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002062 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002063
2064 def shutdown(self):
2065 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002066 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002067 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002068 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002069 self.sched.stop()
2070 self.sched.join()
2071 self.statsd.stop()
2072 self.statsd.join()
2073 self.webapp.stop()
2074 self.webapp.join()
2075 self.rpc.stop()
2076 self.rpc.join()
2077 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002078 self.fake_nodepool.stop()
2079 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002080 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002081 # we whitelist watchdog threads as they have relatively long delays
2082 # before noticing they should exit, but they should exit on their own.
2083 threads = [t for t in threading.enumerate()
2084 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002085 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002086 log_str = ""
2087 for thread_id, stack_frame in sys._current_frames().items():
2088 log_str += "Thread: %s\n" % thread_id
2089 log_str += "".join(traceback.format_stack(stack_frame))
2090 self.log.debug(log_str)
2091 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002092
James E. Blaira002b032017-04-18 10:35:48 -07002093 def assertCleanShutdown(self):
2094 pass
2095
James E. Blairc4ba97a2017-04-19 16:26:24 -07002096 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002097 parts = project.split('/')
2098 path = os.path.join(self.upstream_root, *parts[:-1])
2099 if not os.path.exists(path):
2100 os.makedirs(path)
2101 path = os.path.join(self.upstream_root, project)
2102 repo = git.Repo.init(path)
2103
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002104 with repo.config_writer() as config_writer:
2105 config_writer.set_value('user', 'email', 'user@example.com')
2106 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002107
Clark Boylanb640e052014-04-03 16:41:46 -07002108 repo.index.commit('initial commit')
2109 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002110 if tag:
2111 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002112
James E. Blair97d902e2014-08-21 13:25:56 -07002113 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002114 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002115 repo.git.clean('-x', '-f', '-d')
2116
James E. Blair97d902e2014-08-21 13:25:56 -07002117 def create_branch(self, project, branch):
2118 path = os.path.join(self.upstream_root, project)
2119 repo = git.Repo.init(path)
2120 fn = os.path.join(path, 'README')
2121
2122 branch_head = repo.create_head(branch)
2123 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002124 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002125 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002126 f.close()
2127 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002128 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002129
James E. Blair97d902e2014-08-21 13:25:56 -07002130 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002131 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002132 repo.git.clean('-x', '-f', '-d')
2133
Sachi King9f16d522016-03-16 12:20:45 +11002134 def create_commit(self, project):
2135 path = os.path.join(self.upstream_root, project)
2136 repo = git.Repo(path)
2137 repo.head.reference = repo.heads['master']
2138 file_name = os.path.join(path, 'README')
2139 with open(file_name, 'a') as f:
2140 f.write('creating fake commit\n')
2141 repo.index.add([file_name])
2142 commit = repo.index.commit('Creating a fake commit')
2143 return commit.hexsha
2144
James E. Blairf4a5f022017-04-18 14:01:10 -07002145 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002146 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002147 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002148 while len(self.builds):
2149 self.release(self.builds[0])
2150 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002151 i += 1
2152 if count is not None and i >= count:
2153 break
James E. Blairb8c16472015-05-05 14:55:26 -07002154
Clark Boylanb640e052014-04-03 16:41:46 -07002155 def release(self, job):
2156 if isinstance(job, FakeBuild):
2157 job.release()
2158 else:
2159 job.waiting = False
2160 self.log.debug("Queued job %s released" % job.unique)
2161 self.gearman_server.wakeConnections()
2162
2163 def getParameter(self, job, name):
2164 if isinstance(job, FakeBuild):
2165 return job.parameters[name]
2166 else:
2167 parameters = json.loads(job.arguments)
2168 return parameters[name]
2169
Clark Boylanb640e052014-04-03 16:41:46 -07002170 def haveAllBuildsReported(self):
2171 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002172 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002173 return False
2174 # Find out if every build that the worker has completed has been
2175 # reported back to Zuul. If it hasn't then that means a Gearman
2176 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002177 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002178 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002179 if not zbuild:
2180 # It has already been reported
2181 continue
2182 # It hasn't been reported yet.
2183 return False
2184 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002185 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002186 if connection.state == 'GRAB_WAIT':
2187 return False
2188 return True
2189
2190 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002191 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002192 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002193 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002194 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002195 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002196 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002197 for j in conn.related_jobs.values():
2198 if j.unique == build.uuid:
2199 client_job = j
2200 break
2201 if not client_job:
2202 self.log.debug("%s is not known to the gearman client" %
2203 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002204 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002205 if not client_job.handle:
2206 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002207 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002208 server_job = self.gearman_server.jobs.get(client_job.handle)
2209 if not server_job:
2210 self.log.debug("%s is not known to the gearman server" %
2211 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002212 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002213 if not hasattr(server_job, 'waiting'):
2214 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002215 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002216 if server_job.waiting:
2217 continue
James E. Blair17302972016-08-10 16:11:42 -07002218 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002219 self.log.debug("%s has not reported start" % build)
2220 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002221 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002222 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002223 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002224 if worker_build:
2225 if worker_build.isWaiting():
2226 continue
2227 else:
2228 self.log.debug("%s is running" % worker_build)
2229 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002230 else:
James E. Blair962220f2016-08-03 11:22:38 -07002231 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002232 return False
James E. Blaira002b032017-04-18 10:35:48 -07002233 for (build_uuid, job_worker) in \
2234 self.executor_server.job_workers.items():
2235 if build_uuid not in seen_builds:
2236 self.log.debug("%s is not finalized" % build_uuid)
2237 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002238 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002239
James E. Blairdce6cea2016-12-20 16:45:32 -08002240 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002241 if self.fake_nodepool.paused:
2242 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002243 if self.sched.nodepool.requests:
2244 return False
2245 return True
2246
Jan Hruban6b71aff2015-10-22 16:58:08 +02002247 def eventQueuesEmpty(self):
2248 for queue in self.event_queues:
2249 yield queue.empty()
2250
2251 def eventQueuesJoin(self):
2252 for queue in self.event_queues:
2253 queue.join()
2254
Clark Boylanb640e052014-04-03 16:41:46 -07002255 def waitUntilSettled(self):
2256 self.log.debug("Waiting until settled...")
2257 start = time.time()
2258 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002259 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002260 self.log.error("Timeout waiting for Zuul to settle")
2261 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002262 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002263 self.log.error(" %s: %s" % (queue, queue.empty()))
2264 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002265 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002266 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002267 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002268 self.log.error("All requests completed: %s" %
2269 (self.areAllNodeRequestsComplete(),))
2270 self.log.error("Merge client jobs: %s" %
2271 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002272 raise Exception("Timeout waiting for Zuul to settle")
2273 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002274
Paul Belanger174a8272017-03-14 13:20:10 -04002275 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002276 # have all build states propogated to zuul?
2277 if self.haveAllBuildsReported():
2278 # Join ensures that the queue is empty _and_ events have been
2279 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002280 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002281 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002282 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002283 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002284 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002285 self.areAllNodeRequestsComplete() and
2286 all(self.eventQueuesEmpty())):
2287 # The queue empty check is placed at the end to
2288 # ensure that if a component adds an event between
2289 # when locked the run handler and checked that the
2290 # components were stable, we don't erroneously
2291 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002292 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002293 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002294 self.log.debug("...settled.")
2295 return
2296 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002297 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002298 self.sched.wake_event.wait(0.1)
2299
2300 def countJobResults(self, jobs, result):
2301 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002302 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002303
James E. Blair96c6bf82016-01-15 16:20:40 -08002304 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002305 for job in self.history:
2306 if (job.name == name and
2307 (project is None or
2308 job.parameters['ZUUL_PROJECT'] == project)):
2309 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002310 raise Exception("Unable to find job %s in history" % name)
2311
2312 def assertEmptyQueues(self):
2313 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002314 for tenant in self.sched.abide.tenants.values():
2315 for pipeline in tenant.layout.pipelines.values():
2316 for queue in pipeline.queues:
2317 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002318 print('pipeline %s queue %s contents %s' % (
2319 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002320 self.assertEqual(len(queue.queue), 0,
2321 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002322
2323 def assertReportedStat(self, key, value=None, kind=None):
2324 start = time.time()
2325 while time.time() < (start + 5):
2326 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002327 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002328 if key == k:
2329 if value is None and kind is None:
2330 return
2331 elif value:
2332 if value == v:
2333 return
2334 elif kind:
2335 if v.endswith('|' + kind):
2336 return
2337 time.sleep(0.1)
2338
Clark Boylanb640e052014-04-03 16:41:46 -07002339 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002340
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002341 def assertBuilds(self, builds):
2342 """Assert that the running builds are as described.
2343
2344 The list of running builds is examined and must match exactly
2345 the list of builds described by the input.
2346
2347 :arg list builds: A list of dictionaries. Each item in the
2348 list must match the corresponding build in the build
2349 history, and each element of the dictionary must match the
2350 corresponding attribute of the build.
2351
2352 """
James E. Blair3158e282016-08-19 09:34:11 -07002353 try:
2354 self.assertEqual(len(self.builds), len(builds))
2355 for i, d in enumerate(builds):
2356 for k, v in d.items():
2357 self.assertEqual(
2358 getattr(self.builds[i], k), v,
2359 "Element %i in builds does not match" % (i,))
2360 except Exception:
2361 for build in self.builds:
2362 self.log.error("Running build: %s" % build)
2363 else:
2364 self.log.error("No running builds")
2365 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002366
James E. Blairb536ecc2016-08-31 10:11:42 -07002367 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002368 """Assert that the completed builds are as described.
2369
2370 The list of completed builds is examined and must match
2371 exactly the list of builds described by the input.
2372
2373 :arg list history: A list of dictionaries. Each item in the
2374 list must match the corresponding build in the build
2375 history, and each element of the dictionary must match the
2376 corresponding attribute of the build.
2377
James E. Blairb536ecc2016-08-31 10:11:42 -07002378 :arg bool ordered: If true, the history must match the order
2379 supplied, if false, the builds are permitted to have
2380 arrived in any order.
2381
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002382 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002383 def matches(history_item, item):
2384 for k, v in item.items():
2385 if getattr(history_item, k) != v:
2386 return False
2387 return True
James E. Blair3158e282016-08-19 09:34:11 -07002388 try:
2389 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002390 if ordered:
2391 for i, d in enumerate(history):
2392 if not matches(self.history[i], d):
2393 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002394 "Element %i in history does not match %s" %
2395 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002396 else:
2397 unseen = self.history[:]
2398 for i, d in enumerate(history):
2399 found = False
2400 for unseen_item in unseen:
2401 if matches(unseen_item, d):
2402 found = True
2403 unseen.remove(unseen_item)
2404 break
2405 if not found:
2406 raise Exception("No match found for element %i "
2407 "in history" % (i,))
2408 if unseen:
2409 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002410 except Exception:
2411 for build in self.history:
2412 self.log.error("Completed build: %s" % build)
2413 else:
2414 self.log.error("No completed builds")
2415 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002416
James E. Blair6ac368c2016-12-22 18:07:20 -08002417 def printHistory(self):
2418 """Log the build history.
2419
2420 This can be useful during tests to summarize what jobs have
2421 completed.
2422
2423 """
2424 self.log.debug("Build history:")
2425 for build in self.history:
2426 self.log.debug(build)
2427
James E. Blair59fdbac2015-12-07 17:08:06 -08002428 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002429 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2430
James E. Blair9ea70072017-04-19 16:05:30 -07002431 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002432 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002433 if not os.path.exists(root):
2434 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002435 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2436 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002437- tenant:
2438 name: openstack
2439 source:
2440 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002441 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002442 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002443 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002444 - org/project
2445 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002446 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002447 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002448 self.config.set('zuul', 'tenant_config',
2449 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002450 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002451
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002452 def addCommitToRepo(self, project, message, files,
2453 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002454 path = os.path.join(self.upstream_root, project)
2455 repo = git.Repo(path)
2456 repo.head.reference = branch
2457 zuul.merger.merger.reset_repo_to_head(repo)
2458 for fn, content in files.items():
2459 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002460 try:
2461 os.makedirs(os.path.dirname(fn))
2462 except OSError:
2463 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002464 with open(fn, 'w') as f:
2465 f.write(content)
2466 repo.index.add([fn])
2467 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002468 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002469 repo.heads[branch].commit = commit
2470 repo.head.reference = branch
2471 repo.git.clean('-x', '-f', '-d')
2472 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002473 if tag:
2474 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002475 return before
2476
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002477 def commitConfigUpdate(self, project_name, source_name):
2478 """Commit an update to zuul.yaml
2479
2480 This overwrites the zuul.yaml in the specificed project with
2481 the contents specified.
2482
2483 :arg str project_name: The name of the project containing
2484 zuul.yaml (e.g., common-config)
2485
2486 :arg str source_name: The path to the file (underneath the
2487 test fixture directory) whose contents should be used to
2488 replace zuul.yaml.
2489 """
2490
2491 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002492 files = {}
2493 with open(source_path, 'r') as f:
2494 data = f.read()
2495 layout = yaml.safe_load(data)
2496 files['zuul.yaml'] = data
2497 for item in layout:
2498 if 'job' in item:
2499 jobname = item['job']['name']
2500 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002501 before = self.addCommitToRepo(
2502 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002503 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002504 return before
2505
James E. Blair7fc8daa2016-08-08 15:37:15 -07002506 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002507
James E. Blair7fc8daa2016-08-08 15:37:15 -07002508 """Inject a Fake (Gerrit) event.
2509
2510 This method accepts a JSON-encoded event and simulates Zuul
2511 having received it from Gerrit. It could (and should)
2512 eventually apply to any connection type, but is currently only
2513 used with Gerrit connections. The name of the connection is
2514 used to look up the corresponding server, and the event is
2515 simulated as having been received by all Zuul connections
2516 attached to that server. So if two Gerrit connections in Zuul
2517 are connected to the same Gerrit server, and you invoke this
2518 method specifying the name of one of them, the event will be
2519 received by both.
2520
2521 .. note::
2522
2523 "self.fake_gerrit.addEvent" calls should be migrated to
2524 this method.
2525
2526 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002527 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002528 :arg str event: The JSON-encoded event.
2529
2530 """
2531 specified_conn = self.connections.connections[connection]
2532 for conn in self.connections.connections.values():
2533 if (isinstance(conn, specified_conn.__class__) and
2534 specified_conn.server == conn.server):
2535 conn.addEvent(event)
2536
James E. Blair3f876d52016-07-22 13:07:14 -07002537
2538class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002539 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002540 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002541
Joshua Heskethd78b4482015-09-14 16:56:34 -06002542
2543class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002544 def setup_config(self):
2545 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002546 for section_name in self.config.sections():
2547 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2548 section_name, re.I)
2549 if not con_match:
2550 continue
2551
2552 if self.config.get(section_name, 'driver') == 'sql':
2553 f = MySQLSchemaFixture()
2554 self.useFixture(f)
2555 if (self.config.get(section_name, 'dburi') ==
2556 '$MYSQL_FIXTURE_DBURI$'):
2557 self.config.set(section_name, 'dburi', f.dburi)