blob: 9bacf21ca190d5f03200b13bd0c71e7ec8ac6015 [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
James E. Blair8b5408c2016-08-08 15:37:46 -0700499 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500 if 'label' in action:
501 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000505
Clark Boylanb640e052014-04-03 16:41:46 -0700506 if 'submit' in action:
507 change.setMerged()
508 if message:
509 change.setReported()
510
511 def query(self, number):
512 change = self.changes.get(int(number))
513 if change:
514 return change.query()
515 return {}
516
James E. Blairc494d542014-08-06 09:23:52 -0700517 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700518 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700519 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800520 if query.startswith('change:'):
521 # Query a specific changeid
522 changeid = query[len('change:'):]
523 l = [change.query() for change in self.changes.values()
524 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700525 elif query.startswith('message:'):
526 # Query the content of a commit message
527 msg = query[len('message:'):].strip()
528 l = [change.query() for change in self.changes.values()
529 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800530 else:
531 # Query all open changes
532 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700533 return l
James E. Blairc494d542014-08-06 09:23:52 -0700534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 pass
537
Joshua Hesketh352264b2015-08-11 23:42:08 +1000538 def getGitUrl(self, project):
539 return os.path.join(self.upstream_root, project.name)
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541
Gregory Haynes4fc12542015-04-22 20:38:06 -0700542class GithubChangeReference(git.Reference):
543 _common_path_default = "refs/pull"
544 _points_to_commits_only = True
545
546
547class FakeGithubPullRequest(object):
548
549 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100550 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551 """Creates a new PR with several commits.
552 Sends an event about opened PR."""
553 self.github = github
554 self.source = github
555 self.number = number
556 self.project = project
557 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100558 self.subject = subject
559 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100561 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700562 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100563 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100564 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700565 self.updated_at = None
566 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100567 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100568 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100577 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700578
Jan Hruban570d01c2016-03-10 21:51:32 +0100579 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100583 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584
585 def getPullRequestOpenedEvent(self):
586 return self._getPullRequestEvent('opened')
587
588 def getPullRequestSynchronizeEvent(self):
589 return self._getPullRequestEvent('synchronize')
590
591 def getPullRequestReopenedEvent(self):
592 return self._getPullRequestEvent('reopened')
593
594 def getPullRequestClosedEvent(self):
595 return self._getPullRequestEvent('closed')
596
597 def addComment(self, message):
598 self.comments.append(message)
599 self._updateTimeStamp()
600
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200601 def getCommentAddedEvent(self, text):
602 name = 'issue_comment'
603 data = {
604 'action': 'created',
605 'issue': {
606 'number': self.number
607 },
608 'comment': {
609 'body': text
610 },
611 'repository': {
612 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100613 },
614 'sender': {
615 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200616 }
617 }
618 return (name, data)
619
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800620 def getReviewAddedEvent(self, review):
621 name = 'pull_request_review'
622 data = {
623 'action': 'submitted',
624 'pull_request': {
625 'number': self.number,
626 'title': self.subject,
627 'updated_at': self.updated_at,
628 'base': {
629 'ref': self.branch,
630 'repo': {
631 'full_name': self.project
632 }
633 },
634 'head': {
635 'sha': self.head_sha
636 }
637 },
638 'review': {
639 'state': review
640 },
641 'repository': {
642 'full_name': self.project
643 },
644 'sender': {
645 'login': 'ghuser'
646 }
647 }
648 return (name, data)
649
Jan Hruban16ad31f2015-11-07 14:39:07 +0100650 def addLabel(self, name):
651 if name not in self.labels:
652 self.labels.append(name)
653 self._updateTimeStamp()
654 return self._getLabelEvent(name)
655
656 def removeLabel(self, name):
657 if name in self.labels:
658 self.labels.remove(name)
659 self._updateTimeStamp()
660 return self._getUnlabelEvent(name)
661
662 def _getLabelEvent(self, label):
663 name = 'pull_request'
664 data = {
665 'action': 'labeled',
666 'pull_request': {
667 'number': self.number,
668 'updated_at': self.updated_at,
669 'base': {
670 'ref': self.branch,
671 'repo': {
672 'full_name': self.project
673 }
674 },
675 'head': {
676 'sha': self.head_sha
677 }
678 },
679 'label': {
680 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100681 },
682 'sender': {
683 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100684 }
685 }
686 return (name, data)
687
688 def _getUnlabelEvent(self, label):
689 name = 'pull_request'
690 data = {
691 'action': 'unlabeled',
692 'pull_request': {
693 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100694 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 'updated_at': self.updated_at,
696 'base': {
697 'ref': self.branch,
698 'repo': {
699 'full_name': self.project
700 }
701 },
702 'head': {
703 'sha': self.head_sha
704 }
705 },
706 'label': {
707 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100708 },
709 'sender': {
710 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100711 }
712 }
713 return (name, data)
714
Gregory Haynes4fc12542015-04-22 20:38:06 -0700715 def _getRepo(self):
716 repo_path = os.path.join(self.upstream_root, self.project)
717 return git.Repo(repo_path)
718
719 def _createPRRef(self):
720 repo = self._getRepo()
721 GithubChangeReference.create(
722 repo, self._getPRReference(), 'refs/tags/init')
723
Jan Hruban570d01c2016-03-10 21:51:32 +0100724 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 repo = self._getRepo()
726 ref = repo.references[self._getPRReference()]
727 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100728 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100730 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 repo.head.reference = ref
732 zuul.merger.merger.reset_repo_to_head(repo)
733 repo.git.clean('-x', '-f', '-d')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 if files:
736 fn = files[0]
737 self.files = files
738 else:
739 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
740 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100741 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 fn = os.path.join(repo.working_dir, fn)
743 f = open(fn, 'w')
744 with open(fn, 'w') as f:
745 f.write("test %s %s\n" %
746 (self.branch, self.number))
747 repo.index.add([fn])
748
749 self.head_sha = repo.index.commit(msg).hexsha
750 repo.head.reference = 'master'
751 zuul.merger.merger.reset_repo_to_head(repo)
752 repo.git.clean('-x', '-f', '-d')
753 repo.heads['master'].checkout()
754
755 def _updateTimeStamp(self):
756 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
757
758 def getPRHeadSha(self):
759 repo = self._getRepo()
760 return repo.references[self._getPRReference()].commit.hexsha
761
Jan Hrubane252a732017-01-03 15:03:09 +0100762 def setStatus(self, state, url, description, context):
763 self.statuses[context] = {
764 'state': state,
765 'url': url,
766 'description': description
767 }
768
769 def _clearStatuses(self):
770 self.statuses = {}
771
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 def _getPRReference(self):
773 return '%s/head' % self.number
774
775 def _getPullRequestEvent(self, action):
776 name = 'pull_request'
777 data = {
778 'action': action,
779 'number': self.number,
780 'pull_request': {
781 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100782 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700783 'updated_at': self.updated_at,
784 'base': {
785 'ref': self.branch,
786 'repo': {
787 'full_name': self.project
788 }
789 },
790 'head': {
791 'sha': self.head_sha
792 }
Jan Hruban3b415922016-02-03 13:10:22 +0100793 },
794 'sender': {
795 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700796 }
797 }
798 return (name, data)
799
800
801class FakeGithubConnection(githubconnection.GithubConnection):
802 log = logging.getLogger("zuul.test.FakeGithubConnection")
803
804 def __init__(self, driver, connection_name, connection_config,
805 upstream_root=None):
806 super(FakeGithubConnection, self).__init__(driver, connection_name,
807 connection_config)
808 self.connection_name = connection_name
809 self.pr_number = 0
810 self.pull_requests = []
811 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100812 self.merge_failure = False
813 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814
Jan Hruban570d01c2016-03-10 21:51:32 +0100815 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 self.pr_number += 1
817 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100818 self, self.pr_number, project, branch, subject, self.upstream_root,
819 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700820 self.pull_requests.append(pull_request)
821 return pull_request
822
Wayne1a78c612015-06-11 17:14:13 -0700823 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
824 if not old_rev:
825 old_rev = '00000000000000000000000000000000'
826 if not new_rev:
827 new_rev = random_sha1()
828 name = 'push'
829 data = {
830 'ref': ref,
831 'before': old_rev,
832 'after': new_rev,
833 'repository': {
834 'full_name': project
835 }
836 }
837 return (name, data)
838
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 def emitEvent(self, event):
840 """Emulates sending the GitHub webhook event to the connection."""
841 port = self.webapp.server.socket.getsockname()[1]
842 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700843 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700844 headers = {'X-Github-Event': name}
845 req = urllib.request.Request(
846 'http://localhost:%s/connection/%s/payload'
847 % (port, self.connection_name),
848 data=payload, headers=headers)
849 urllib.request.urlopen(req)
850
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200851 def getPull(self, project, number):
852 pr = self.pull_requests[number - 1]
853 data = {
854 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100855 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200856 'updated_at': pr.updated_at,
857 'base': {
858 'repo': {
859 'full_name': pr.project
860 },
861 'ref': pr.branch,
862 },
Jan Hruban37615e52015-11-19 14:30:49 +0100863 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200864 'head': {
865 'sha': pr.head_sha
866 }
867 }
868 return data
869
Jan Hruban570d01c2016-03-10 21:51:32 +0100870 def getPullFileNames(self, project, number):
871 pr = self.pull_requests[number - 1]
872 return pr.files
873
Jan Hruban3b415922016-02-03 13:10:22 +0100874 def getUser(self, login):
875 data = {
876 'username': login,
877 'name': 'Github User',
878 'email': 'github.user@example.com'
879 }
880 return data
881
Gregory Haynes4fc12542015-04-22 20:38:06 -0700882 def getGitUrl(self, project):
883 return os.path.join(self.upstream_root, str(project))
884
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200885 def real_getGitUrl(self, project):
886 return super(FakeGithubConnection, self).getGitUrl(project)
887
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888 def getProjectBranches(self, project):
889 """Masks getProjectBranches since we don't have a real github"""
890
891 # just returns master for now
892 return ['master']
893
Jan Hrubane252a732017-01-03 15:03:09 +0100894 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700895 pull_request = self.pull_requests[pr_number - 1]
896 pull_request.addComment(message)
897
Jan Hruban3b415922016-02-03 13:10:22 +0100898 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100899 pull_request = self.pull_requests[pr_number - 1]
900 if self.merge_failure:
901 raise Exception('Pull request was not merged')
902 if self.merge_not_allowed_count > 0:
903 self.merge_not_allowed_count -= 1
904 raise MergeFailure('Merge was not successful due to mergeability'
905 ' conflict')
906 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100907 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100908
Jan Hrubane252a732017-01-03 15:03:09 +0100909 def setCommitStatus(self, project, sha, state,
910 url='', description='', context=''):
911 owner, proj = project.split('/')
912 for pr in self.pull_requests:
913 pr_owner, pr_project = pr.project.split('/')
914 if (pr_owner == owner and pr_project == proj and
915 pr.head_sha == sha):
916 pr.setStatus(state, url, description, context)
917
Jan Hruban16ad31f2015-11-07 14:39:07 +0100918 def labelPull(self, project, pr_number, label):
919 pull_request = self.pull_requests[pr_number - 1]
920 pull_request.addLabel(label)
921
922 def unlabelPull(self, project, pr_number, label):
923 pull_request = self.pull_requests[pr_number - 1]
924 pull_request.removeLabel(label)
925
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926
Clark Boylanb640e052014-04-03 16:41:46 -0700927class BuildHistory(object):
928 def __init__(self, **kw):
929 self.__dict__.update(kw)
930
931 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700932 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
933 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700934
935
936class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200937 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700938 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700939 self.url = url
940
941 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700942 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700943 path = res.path
944 project = '/'.join(path.split('/')[2:-2])
945 ret = '001e# service=git-upload-pack\n'
946 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
947 'multi_ack thin-pack side-band side-band-64k ofs-delta '
948 'shallow no-progress include-tag multi_ack_detailed no-done\n')
949 path = os.path.join(self.upstream_root, project)
950 repo = git.Repo(path)
951 for ref in repo.refs:
952 r = ref.object.hexsha + ' ' + ref.path + '\n'
953 ret += '%04x%s' % (len(r) + 4, r)
954 ret += '0000'
955 return ret
956
957
Clark Boylanb640e052014-04-03 16:41:46 -0700958class FakeStatsd(threading.Thread):
959 def __init__(self):
960 threading.Thread.__init__(self)
961 self.daemon = True
962 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
963 self.sock.bind(('', 0))
964 self.port = self.sock.getsockname()[1]
965 self.wake_read, self.wake_write = os.pipe()
966 self.stats = []
967
968 def run(self):
969 while True:
970 poll = select.poll()
971 poll.register(self.sock, select.POLLIN)
972 poll.register(self.wake_read, select.POLLIN)
973 ret = poll.poll()
974 for (fd, event) in ret:
975 if fd == self.sock.fileno():
976 data = self.sock.recvfrom(1024)
977 if not data:
978 return
979 self.stats.append(data[0])
980 if fd == self.wake_read:
981 return
982
983 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -0700984 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -0700985
986
James E. Blaire1767bc2016-08-02 10:00:27 -0700987class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700988 log = logging.getLogger("zuul.test")
989
Paul Belanger174a8272017-03-14 13:20:10 -0400990 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700991 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400992 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700994 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700995 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700996 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700997 # TODOv3(jeblair): self.node is really "the image of the node
998 # assigned". We should rename it (self.node_image?) if we
999 # keep using it like this, or we may end up exposing more of
1000 # the complexity around multi-node jobs here
1001 # (self.nodes[0].image?)
1002 self.node = None
1003 if len(self.parameters.get('nodes')) == 1:
1004 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001005 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001006 self.pipeline = self.parameters['ZUUL_PIPELINE']
1007 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001008 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001009 self.wait_condition = threading.Condition()
1010 self.waiting = False
1011 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001012 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001013 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001014 self.changes = None
1015 if 'ZUUL_CHANGE_IDS' in self.parameters:
1016 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001017
James E. Blair3158e282016-08-19 09:34:11 -07001018 def __repr__(self):
1019 waiting = ''
1020 if self.waiting:
1021 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001022 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1023 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001024
Clark Boylanb640e052014-04-03 16:41:46 -07001025 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001026 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001027 self.wait_condition.acquire()
1028 self.wait_condition.notify()
1029 self.waiting = False
1030 self.log.debug("Build %s released" % self.unique)
1031 self.wait_condition.release()
1032
1033 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001034 """Return whether this build is being held.
1035
1036 :returns: Whether the build is being held.
1037 :rtype: bool
1038 """
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040 self.wait_condition.acquire()
1041 if self.waiting:
1042 ret = True
1043 else:
1044 ret = False
1045 self.wait_condition.release()
1046 return ret
1047
1048 def _wait(self):
1049 self.wait_condition.acquire()
1050 self.waiting = True
1051 self.log.debug("Build %s waiting" % self.unique)
1052 self.wait_condition.wait()
1053 self.wait_condition.release()
1054
1055 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001056 self.log.debug('Running build %s' % self.unique)
1057
Paul Belanger174a8272017-03-14 13:20:10 -04001058 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001059 self.log.debug('Holding build %s' % self.unique)
1060 self._wait()
1061 self.log.debug("Build %s continuing" % self.unique)
1062
James E. Blair412fba82017-01-26 15:00:50 -08001063 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001064 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001065 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001066 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001067 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001068 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001069 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001070
James E. Blaire1767bc2016-08-02 10:00:27 -07001071 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001072
James E. Blaira5dba232016-08-08 15:53:24 -07001073 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001074 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001075 for change in changes:
1076 if self.hasChanges(change):
1077 return True
1078 return False
1079
James E. Blaire7b99a02016-08-05 14:27:34 -07001080 def hasChanges(self, *changes):
1081 """Return whether this build has certain changes in its git repos.
1082
1083 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001084 are expected to be present (in order) in the git repository of
1085 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001086
1087 :returns: Whether the build has the indicated changes.
1088 :rtype: bool
1089
1090 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001091 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001092 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001093 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001094 try:
1095 repo = git.Repo(path)
1096 except NoSuchPathError as e:
1097 self.log.debug('%s' % e)
1098 return False
1099 ref = self.parameters['ZUUL_REF']
1100 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1101 commit_message = '%s-1' % change.subject
1102 self.log.debug("Checking if build %s has changes; commit_message "
1103 "%s; repo_messages %s" % (self, commit_message,
1104 repo_messages))
1105 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001106 self.log.debug(" messages do not match")
1107 return False
1108 self.log.debug(" OK")
1109 return True
1110
Clark Boylanb640e052014-04-03 16:41:46 -07001111
Paul Belanger174a8272017-03-14 13:20:10 -04001112class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1113 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001114
Paul Belanger174a8272017-03-14 13:20:10 -04001115 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001116 they will report that they have started but then pause until
1117 released before reporting completion. This attribute may be
1118 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001119 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 be explicitly released.
1121
1122 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001123 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001124 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001125 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001126 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.hold_jobs_in_build = False
1128 self.lock = threading.Lock()
1129 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001130 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001131 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001132 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001133
James E. Blaira5dba232016-08-08 15:53:24 -07001134 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001135 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001136
1137 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001138 :arg Change change: The :py:class:`~tests.base.FakeChange`
1139 instance which should cause the job to fail. This job
1140 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001141
1142 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001143 l = self.fail_tests.get(name, [])
1144 l.append(change)
1145 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001146
James E. Blair962220f2016-08-03 11:22:38 -07001147 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001148 """Release a held build.
1149
1150 :arg str regex: A regular expression which, if supplied, will
1151 cause only builds with matching names to be released. If
1152 not supplied, all builds will be released.
1153
1154 """
James E. Blair962220f2016-08-03 11:22:38 -07001155 builds = self.running_builds[:]
1156 self.log.debug("Releasing build %s (%s)" % (regex,
1157 len(self.running_builds)))
1158 for build in builds:
1159 if not regex or re.match(regex, build.name):
1160 self.log.debug("Releasing build %s" %
1161 (build.parameters['ZUUL_UUID']))
1162 build.release()
1163 else:
1164 self.log.debug("Not releasing build %s" %
1165 (build.parameters['ZUUL_UUID']))
1166 self.log.debug("Done releasing builds %s (%s)" %
1167 (regex, len(self.running_builds)))
1168
Paul Belanger174a8272017-03-14 13:20:10 -04001169 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001170 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001171 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001172 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001173 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001174 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001175 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001176 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001177 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1178 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001179
1180 def stopJob(self, job):
1181 self.log.debug("handle stop")
1182 parameters = json.loads(job.arguments)
1183 uuid = parameters['uuid']
1184 for build in self.running_builds:
1185 if build.unique == uuid:
1186 build.aborted = True
1187 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001188 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001189
James E. Blaira002b032017-04-18 10:35:48 -07001190 def stop(self):
1191 for build in self.running_builds:
1192 build.release()
1193 super(RecordingExecutorServer, self).stop()
1194
Joshua Hesketh50c21782016-10-13 21:34:14 +11001195
Paul Belanger174a8272017-03-14 13:20:10 -04001196class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001197 def doMergeChanges(self, items):
1198 # Get a merger in order to update the repos involved in this job.
1199 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1200 if not commit: # merge conflict
1201 self.recordResult('MERGER_FAILURE')
1202 return commit
1203
1204 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001205 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001206 self.executor_server.lock.acquire()
1207 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001208 BuildHistory(name=build.name, result=result, changes=build.changes,
1209 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001210 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001211 pipeline=build.parameters['ZUUL_PIPELINE'])
1212 )
Paul Belanger174a8272017-03-14 13:20:10 -04001213 self.executor_server.running_builds.remove(build)
1214 del self.executor_server.job_builds[self.job.unique]
1215 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001216
1217 def runPlaybooks(self, args):
1218 build = self.executor_server.job_builds[self.job.unique]
1219 build.jobdir = self.jobdir
1220
1221 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1222 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001223 return result
1224
Monty Taylore6562aa2017-02-20 07:37:39 -05001225 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001226 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001229 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001230 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001231 else:
1232 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001233 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001234
James E. Blairad8dca02017-02-21 11:48:32 -05001235 def getHostList(self, args):
1236 self.log.debug("hostlist")
1237 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001238 for host in hosts:
1239 host['host_vars']['ansible_connection'] = 'local'
1240
1241 hosts.append(dict(
1242 name='localhost',
1243 host_vars=dict(ansible_connection='local'),
1244 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001245 return hosts
1246
James E. Blairf5dbd002015-12-23 15:26:17 -08001247
Clark Boylanb640e052014-04-03 16:41:46 -07001248class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001249 """A Gearman server for use in tests.
1250
1251 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1252 added to the queue but will not be distributed to workers
1253 until released. This attribute may be changed at any time and
1254 will take effect for subsequently enqueued jobs, but
1255 previously held jobs will still need to be explicitly
1256 released.
1257
1258 """
1259
Clark Boylanb640e052014-04-03 16:41:46 -07001260 def __init__(self):
1261 self.hold_jobs_in_queue = False
1262 super(FakeGearmanServer, self).__init__(0)
1263
1264 def getJobForConnection(self, connection, peek=False):
1265 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1266 for job in queue:
1267 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001268 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001269 job.waiting = self.hold_jobs_in_queue
1270 else:
1271 job.waiting = False
1272 if job.waiting:
1273 continue
1274 if job.name in connection.functions:
1275 if not peek:
1276 queue.remove(job)
1277 connection.related_jobs[job.handle] = job
1278 job.worker_connection = connection
1279 job.running = True
1280 return job
1281 return None
1282
1283 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001284 """Release a held job.
1285
1286 :arg str regex: A regular expression which, if supplied, will
1287 cause only jobs with matching names to be released. If
1288 not supplied, all jobs will be released.
1289 """
Clark Boylanb640e052014-04-03 16:41:46 -07001290 released = False
1291 qlen = (len(self.high_queue) + len(self.normal_queue) +
1292 len(self.low_queue))
1293 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1294 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001295 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001296 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001297 parameters = json.loads(job.arguments)
1298 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.log.debug("releasing queued job %s" %
1300 job.unique)
1301 job.waiting = False
1302 released = True
1303 else:
1304 self.log.debug("not releasing queued job %s" %
1305 job.unique)
1306 if released:
1307 self.wakeConnections()
1308 qlen = (len(self.high_queue) + len(self.normal_queue) +
1309 len(self.low_queue))
1310 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1311
1312
1313class FakeSMTP(object):
1314 log = logging.getLogger('zuul.FakeSMTP')
1315
1316 def __init__(self, messages, server, port):
1317 self.server = server
1318 self.port = port
1319 self.messages = messages
1320
1321 def sendmail(self, from_email, to_email, msg):
1322 self.log.info("Sending email from %s, to %s, with msg %s" % (
1323 from_email, to_email, msg))
1324
1325 headers = msg.split('\n\n', 1)[0]
1326 body = msg.split('\n\n', 1)[1]
1327
1328 self.messages.append(dict(
1329 from_email=from_email,
1330 to_email=to_email,
1331 msg=msg,
1332 headers=headers,
1333 body=body,
1334 ))
1335
1336 return True
1337
1338 def quit(self):
1339 return True
1340
1341
James E. Blairdce6cea2016-12-20 16:45:32 -08001342class FakeNodepool(object):
1343 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001344 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001345
1346 log = logging.getLogger("zuul.test.FakeNodepool")
1347
1348 def __init__(self, host, port, chroot):
1349 self.client = kazoo.client.KazooClient(
1350 hosts='%s:%s%s' % (host, port, chroot))
1351 self.client.start()
1352 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001353 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001354 self.thread = threading.Thread(target=self.run)
1355 self.thread.daemon = True
1356 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001357 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001358
1359 def stop(self):
1360 self._running = False
1361 self.thread.join()
1362 self.client.stop()
1363 self.client.close()
1364
1365 def run(self):
1366 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001367 try:
1368 self._run()
1369 except Exception:
1370 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001371 time.sleep(0.1)
1372
1373 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001374 if self.paused:
1375 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001376 for req in self.getNodeRequests():
1377 self.fulfillRequest(req)
1378
1379 def getNodeRequests(self):
1380 try:
1381 reqids = self.client.get_children(self.REQUEST_ROOT)
1382 except kazoo.exceptions.NoNodeError:
1383 return []
1384 reqs = []
1385 for oid in sorted(reqids):
1386 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001387 try:
1388 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001389 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001390 data['_oid'] = oid
1391 reqs.append(data)
1392 except kazoo.exceptions.NoNodeError:
1393 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001394 return reqs
1395
James E. Blaire18d4602017-01-05 11:17:28 -08001396 def getNodes(self):
1397 try:
1398 nodeids = self.client.get_children(self.NODE_ROOT)
1399 except kazoo.exceptions.NoNodeError:
1400 return []
1401 nodes = []
1402 for oid in sorted(nodeids):
1403 path = self.NODE_ROOT + '/' + oid
1404 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001405 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001406 data['_oid'] = oid
1407 try:
1408 lockfiles = self.client.get_children(path + '/lock')
1409 except kazoo.exceptions.NoNodeError:
1410 lockfiles = []
1411 if lockfiles:
1412 data['_lock'] = True
1413 else:
1414 data['_lock'] = False
1415 nodes.append(data)
1416 return nodes
1417
James E. Blaira38c28e2017-01-04 10:33:20 -08001418 def makeNode(self, request_id, node_type):
1419 now = time.time()
1420 path = '/nodepool/nodes/'
1421 data = dict(type=node_type,
1422 provider='test-provider',
1423 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001424 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001425 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001426 public_ipv4='127.0.0.1',
1427 private_ipv4=None,
1428 public_ipv6=None,
1429 allocated_to=request_id,
1430 state='ready',
1431 state_time=now,
1432 created_time=now,
1433 updated_time=now,
1434 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001435 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001436 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001437 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001438 path = self.client.create(path, data,
1439 makepath=True,
1440 sequence=True)
1441 nodeid = path.split("/")[-1]
1442 return nodeid
1443
James E. Blair6ab79e02017-01-06 10:10:17 -08001444 def addFailRequest(self, request):
1445 self.fail_requests.add(request['_oid'])
1446
James E. Blairdce6cea2016-12-20 16:45:32 -08001447 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001448 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001449 return
1450 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001451 oid = request['_oid']
1452 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001453
James E. Blair6ab79e02017-01-06 10:10:17 -08001454 if oid in self.fail_requests:
1455 request['state'] = 'failed'
1456 else:
1457 request['state'] = 'fulfilled'
1458 nodes = []
1459 for node in request['node_types']:
1460 nodeid = self.makeNode(oid, node)
1461 nodes.append(nodeid)
1462 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001463
James E. Blaira38c28e2017-01-04 10:33:20 -08001464 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001465 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001466 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001467 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001468 try:
1469 self.client.set(path, data)
1470 except kazoo.exceptions.NoNodeError:
1471 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001472
1473
James E. Blair498059b2016-12-20 13:50:13 -08001474class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001475 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001476 super(ChrootedKazooFixture, self).__init__()
1477
1478 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1479 if ':' in zk_host:
1480 host, port = zk_host.split(':')
1481 else:
1482 host = zk_host
1483 port = None
1484
1485 self.zookeeper_host = host
1486
1487 if not port:
1488 self.zookeeper_port = 2181
1489 else:
1490 self.zookeeper_port = int(port)
1491
Clark Boylan621ec9a2017-04-07 17:41:33 -07001492 self.test_id = test_id
1493
James E. Blair498059b2016-12-20 13:50:13 -08001494 def _setUp(self):
1495 # Make sure the test chroot paths do not conflict
1496 random_bits = ''.join(random.choice(string.ascii_lowercase +
1497 string.ascii_uppercase)
1498 for x in range(8))
1499
Clark Boylan621ec9a2017-04-07 17:41:33 -07001500 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001501 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1502
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001503 self.addCleanup(self._cleanup)
1504
James E. Blair498059b2016-12-20 13:50:13 -08001505 # Ensure the chroot path exists and clean up any pre-existing znodes.
1506 _tmp_client = kazoo.client.KazooClient(
1507 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1508 _tmp_client.start()
1509
1510 if _tmp_client.exists(self.zookeeper_chroot):
1511 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1512
1513 _tmp_client.ensure_path(self.zookeeper_chroot)
1514 _tmp_client.stop()
1515 _tmp_client.close()
1516
James E. Blair498059b2016-12-20 13:50:13 -08001517 def _cleanup(self):
1518 '''Remove the chroot path.'''
1519 # Need a non-chroot'ed client to remove the chroot path
1520 _tmp_client = kazoo.client.KazooClient(
1521 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1522 _tmp_client.start()
1523 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1524 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001525 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001526
1527
Joshua Heskethd78b4482015-09-14 16:56:34 -06001528class MySQLSchemaFixture(fixtures.Fixture):
1529 def setUp(self):
1530 super(MySQLSchemaFixture, self).setUp()
1531
1532 random_bits = ''.join(random.choice(string.ascii_lowercase +
1533 string.ascii_uppercase)
1534 for x in range(8))
1535 self.name = '%s_%s' % (random_bits, os.getpid())
1536 self.passwd = uuid.uuid4().hex
1537 db = pymysql.connect(host="localhost",
1538 user="openstack_citest",
1539 passwd="openstack_citest",
1540 db="openstack_citest")
1541 cur = db.cursor()
1542 cur.execute("create database %s" % self.name)
1543 cur.execute(
1544 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1545 (self.name, self.name, self.passwd))
1546 cur.execute("flush privileges")
1547
1548 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1549 self.passwd,
1550 self.name)
1551 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1552 self.addCleanup(self.cleanup)
1553
1554 def cleanup(self):
1555 db = pymysql.connect(host="localhost",
1556 user="openstack_citest",
1557 passwd="openstack_citest",
1558 db="openstack_citest")
1559 cur = db.cursor()
1560 cur.execute("drop database %s" % self.name)
1561 cur.execute("drop user '%s'@'localhost'" % self.name)
1562 cur.execute("flush privileges")
1563
1564
Maru Newby3fe5f852015-01-13 04:22:14 +00001565class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001566 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001567 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001568
James E. Blair1c236df2017-02-01 14:07:24 -08001569 def attachLogs(self, *args):
1570 def reader():
1571 self._log_stream.seek(0)
1572 while True:
1573 x = self._log_stream.read(4096)
1574 if not x:
1575 break
1576 yield x.encode('utf8')
1577 content = testtools.content.content_from_reader(
1578 reader,
1579 testtools.content_type.UTF8_TEXT,
1580 False)
1581 self.addDetail('logging', content)
1582
Clark Boylanb640e052014-04-03 16:41:46 -07001583 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001584 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001585 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1586 try:
1587 test_timeout = int(test_timeout)
1588 except ValueError:
1589 # If timeout value is invalid do not set a timeout.
1590 test_timeout = 0
1591 if test_timeout > 0:
1592 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1593
1594 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1595 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1596 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1597 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1598 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1599 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1600 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1601 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1602 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1603 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001604 self._log_stream = StringIO()
1605 self.addOnException(self.attachLogs)
1606 else:
1607 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001608
James E. Blair1c236df2017-02-01 14:07:24 -08001609 handler = logging.StreamHandler(self._log_stream)
1610 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1611 '%(levelname)-8s %(message)s')
1612 handler.setFormatter(formatter)
1613
1614 logger = logging.getLogger()
1615 logger.setLevel(logging.DEBUG)
1616 logger.addHandler(handler)
1617
Clark Boylan3410d532017-04-25 12:35:29 -07001618 # Make sure we don't carry old handlers around in process state
1619 # which slows down test runs
1620 self.addCleanup(logger.removeHandler, handler)
1621 self.addCleanup(handler.close)
1622 self.addCleanup(handler.flush)
1623
James E. Blair1c236df2017-02-01 14:07:24 -08001624 # NOTE(notmorgan): Extract logging overrides for specific
1625 # libraries from the OS_LOG_DEFAULTS env and create loggers
1626 # for each. This is used to limit the output during test runs
1627 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001628 log_defaults_from_env = os.environ.get(
1629 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001630 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001631
James E. Blairdce6cea2016-12-20 16:45:32 -08001632 if log_defaults_from_env:
1633 for default in log_defaults_from_env.split(','):
1634 try:
1635 name, level_str = default.split('=', 1)
1636 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001637 logger = logging.getLogger(name)
1638 logger.setLevel(level)
1639 logger.addHandler(handler)
1640 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001641 except ValueError:
1642 # NOTE(notmorgan): Invalid format of the log default,
1643 # skip and don't try and apply a logger for the
1644 # specified module
1645 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001646
Maru Newby3fe5f852015-01-13 04:22:14 +00001647
1648class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001649 """A test case with a functioning Zuul.
1650
1651 The following class variables are used during test setup and can
1652 be overidden by subclasses but are effectively read-only once a
1653 test method starts running:
1654
1655 :cvar str config_file: This points to the main zuul config file
1656 within the fixtures directory. Subclasses may override this
1657 to obtain a different behavior.
1658
1659 :cvar str tenant_config_file: This is the tenant config file
1660 (which specifies from what git repos the configuration should
1661 be loaded). It defaults to the value specified in
1662 `config_file` but can be overidden by subclasses to obtain a
1663 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001664 configuration. See also the :py:func:`simple_layout`
1665 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001666
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001667 :cvar bool create_project_keys: Indicates whether Zuul should
1668 auto-generate keys for each project, or whether the test
1669 infrastructure should insert dummy keys to save time during
1670 startup. Defaults to False.
1671
James E. Blaire7b99a02016-08-05 14:27:34 -07001672 The following are instance variables that are useful within test
1673 methods:
1674
1675 :ivar FakeGerritConnection fake_<connection>:
1676 A :py:class:`~tests.base.FakeGerritConnection` will be
1677 instantiated for each connection present in the config file
1678 and stored here. For instance, `fake_gerrit` will hold the
1679 FakeGerritConnection object for a connection named `gerrit`.
1680
1681 :ivar FakeGearmanServer gearman_server: An instance of
1682 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1683 server that all of the Zuul components in this test use to
1684 communicate with each other.
1685
Paul Belanger174a8272017-03-14 13:20:10 -04001686 :ivar RecordingExecutorServer executor_server: An instance of
1687 :py:class:`~tests.base.RecordingExecutorServer` which is the
1688 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001689
1690 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1691 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001692 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001693 list upon completion.
1694
1695 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1696 objects representing completed builds. They are appended to
1697 the list in the order they complete.
1698
1699 """
1700
James E. Blair83005782015-12-11 14:46:03 -08001701 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001702 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001703 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001704
1705 def _startMerger(self):
1706 self.merge_server = zuul.merger.server.MergeServer(self.config,
1707 self.connections)
1708 self.merge_server.start()
1709
Maru Newby3fe5f852015-01-13 04:22:14 +00001710 def setUp(self):
1711 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001712
1713 self.setupZK()
1714
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001715 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001716 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001717 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1718 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001719 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001720 tmp_root = tempfile.mkdtemp(
1721 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001722 self.test_root = os.path.join(tmp_root, "zuul-test")
1723 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001724 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001725 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001726 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001727
1728 if os.path.exists(self.test_root):
1729 shutil.rmtree(self.test_root)
1730 os.makedirs(self.test_root)
1731 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001732 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001733
1734 # Make per test copy of Configuration.
1735 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001736 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001737 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001738 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001739 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001740 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001741 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001742
Clark Boylanb640e052014-04-03 16:41:46 -07001743 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001744 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1745 # see: https://github.com/jsocol/pystatsd/issues/61
1746 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001747 os.environ['STATSD_PORT'] = str(self.statsd.port)
1748 self.statsd.start()
1749 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001750 reload_module(statsd)
1751 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001752
1753 self.gearman_server = FakeGearmanServer()
1754
1755 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001756 self.log.info("Gearman server on port %s" %
1757 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001758
James E. Blaire511d2f2016-12-08 15:22:26 -08001759 gerritsource.GerritSource.replication_timeout = 1.5
1760 gerritsource.GerritSource.replication_retry_interval = 0.5
1761 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001762
Joshua Hesketh352264b2015-08-11 23:42:08 +10001763 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001764
Jan Hruban7083edd2015-08-21 14:00:54 +02001765 self.webapp = zuul.webapp.WebApp(
1766 self.sched, port=0, listen_address='127.0.0.1')
1767
Jan Hruban6b71aff2015-10-22 16:58:08 +02001768 self.event_queues = [
1769 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001770 self.sched.trigger_event_queue,
1771 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001772 ]
1773
James E. Blairfef78942016-03-11 16:28:56 -08001774 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001775 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001776
Clark Boylanb640e052014-04-03 16:41:46 -07001777 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001778 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001779 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001780 return FakeURLOpener(self.upstream_root, *args, **kw)
1781
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001782 old_urlopen = urllib.request.urlopen
1783 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001784
James E. Blair3f876d52016-07-22 13:07:14 -07001785 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001786
Paul Belanger174a8272017-03-14 13:20:10 -04001787 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001788 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001789 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001790 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001791 _test_root=self.test_root,
1792 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001793 self.executor_server.start()
1794 self.history = self.executor_server.build_history
1795 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001796
Paul Belanger174a8272017-03-14 13:20:10 -04001797 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001798 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001799 self.merge_client = zuul.merger.client.MergeClient(
1800 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001801 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001802 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001803 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001804
James E. Blair0d5a36e2017-02-21 10:53:44 -05001805 self.fake_nodepool = FakeNodepool(
1806 self.zk_chroot_fixture.zookeeper_host,
1807 self.zk_chroot_fixture.zookeeper_port,
1808 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001809
Paul Belanger174a8272017-03-14 13:20:10 -04001810 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001811 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001812 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001813 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001814
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001815 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001816
1817 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001818 self.webapp.start()
1819 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001820 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001821 # Cleanups are run in reverse order
1822 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001823 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001824 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001825
James E. Blairb9c0d772017-03-03 14:34:49 -08001826 self.sched.reconfigure(self.config)
1827 self.sched.resume()
1828
James E. Blairfef78942016-03-11 16:28:56 -08001829 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001830 # Set up gerrit related fakes
1831 # Set a changes database so multiple FakeGerrit's can report back to
1832 # a virtual canonical database given by the configured hostname
1833 self.gerrit_changes_dbs = {}
1834
1835 def getGerritConnection(driver, name, config):
1836 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1837 con = FakeGerritConnection(driver, name, config,
1838 changes_db=db,
1839 upstream_root=self.upstream_root)
1840 self.event_queues.append(con.event_queue)
1841 setattr(self, 'fake_' + name, con)
1842 return con
1843
1844 self.useFixture(fixtures.MonkeyPatch(
1845 'zuul.driver.gerrit.GerritDriver.getConnection',
1846 getGerritConnection))
1847
Gregory Haynes4fc12542015-04-22 20:38:06 -07001848 def getGithubConnection(driver, name, config):
1849 con = FakeGithubConnection(driver, name, config,
1850 upstream_root=self.upstream_root)
1851 setattr(self, 'fake_' + name, con)
1852 return con
1853
1854 self.useFixture(fixtures.MonkeyPatch(
1855 'zuul.driver.github.GithubDriver.getConnection',
1856 getGithubConnection))
1857
James E. Blaire511d2f2016-12-08 15:22:26 -08001858 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001859 # TODO(jhesketh): This should come from lib.connections for better
1860 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001861 # Register connections from the config
1862 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001863
Joshua Hesketh352264b2015-08-11 23:42:08 +10001864 def FakeSMTPFactory(*args, **kw):
1865 args = [self.smtp_messages] + list(args)
1866 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001867
Joshua Hesketh352264b2015-08-11 23:42:08 +10001868 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001869
James E. Blaire511d2f2016-12-08 15:22:26 -08001870 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001871 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001872 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001873
James E. Blair83005782015-12-11 14:46:03 -08001874 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001875 # This creates the per-test configuration object. It can be
1876 # overriden by subclasses, but should not need to be since it
1877 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001878 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001879 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001880
1881 if not self.setupSimpleLayout():
1882 if hasattr(self, 'tenant_config_file'):
1883 self.config.set('zuul', 'tenant_config',
1884 self.tenant_config_file)
1885 git_path = os.path.join(
1886 os.path.dirname(
1887 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1888 'git')
1889 if os.path.exists(git_path):
1890 for reponame in os.listdir(git_path):
1891 project = reponame.replace('_', '/')
1892 self.copyDirToRepo(project,
1893 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001894 self.setupAllProjectKeys()
1895
James E. Blair06cc3922017-04-19 10:08:10 -07001896 def setupSimpleLayout(self):
1897 # If the test method has been decorated with a simple_layout,
1898 # use that instead of the class tenant_config_file. Set up a
1899 # single config-project with the specified layout, and
1900 # initialize repos for all of the 'project' entries which
1901 # appear in the layout.
1902 test_name = self.id().split('.')[-1]
1903 test = getattr(self, test_name)
1904 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001905 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001906 else:
1907 return False
1908
James E. Blairb70e55a2017-04-19 12:57:02 -07001909 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001910 path = os.path.join(FIXTURE_DIR, path)
1911 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001912 data = f.read()
1913 layout = yaml.safe_load(data)
1914 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001915 untrusted_projects = []
1916 for item in layout:
1917 if 'project' in item:
1918 name = item['project']['name']
1919 untrusted_projects.append(name)
1920 self.init_repo(name)
1921 self.addCommitToRepo(name, 'initial commit',
1922 files={'README': ''},
1923 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001924 if 'job' in item:
1925 jobname = item['job']['name']
1926 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001927
1928 root = os.path.join(self.test_root, "config")
1929 if not os.path.exists(root):
1930 os.makedirs(root)
1931 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1932 config = [{'tenant':
1933 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001934 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001935 {'config-projects': ['common-config'],
1936 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001937 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001938 f.close()
1939 self.config.set('zuul', 'tenant_config',
1940 os.path.join(FIXTURE_DIR, f.name))
1941
1942 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001943 self.addCommitToRepo('common-config', 'add content from fixture',
1944 files, branch='master', tag='init')
1945
1946 return True
1947
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001948 def setupAllProjectKeys(self):
1949 if self.create_project_keys:
1950 return
1951
1952 path = self.config.get('zuul', 'tenant_config')
1953 with open(os.path.join(FIXTURE_DIR, path)) as f:
1954 tenant_config = yaml.safe_load(f.read())
1955 for tenant in tenant_config:
1956 sources = tenant['tenant']['source']
1957 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001958 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001959 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001960 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001961 self.setupProjectKeys(source, project)
1962
1963 def setupProjectKeys(self, source, project):
1964 # Make sure we set up an RSA key for the project so that we
1965 # don't spend time generating one:
1966
1967 key_root = os.path.join(self.state_root, 'keys')
1968 if not os.path.isdir(key_root):
1969 os.mkdir(key_root, 0o700)
1970 private_key_file = os.path.join(key_root, source, project + '.pem')
1971 private_key_dir = os.path.dirname(private_key_file)
1972 self.log.debug("Installing test keys for project %s at %s" % (
1973 project, private_key_file))
1974 if not os.path.isdir(private_key_dir):
1975 os.makedirs(private_key_dir)
1976 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1977 with open(private_key_file, 'w') as o:
1978 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001979
James E. Blair498059b2016-12-20 13:50:13 -08001980 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001981 self.zk_chroot_fixture = self.useFixture(
1982 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001983 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001984 self.zk_chroot_fixture.zookeeper_host,
1985 self.zk_chroot_fixture.zookeeper_port,
1986 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001987
James E. Blair96c6bf82016-01-15 16:20:40 -08001988 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001989 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001990
1991 files = {}
1992 for (dirpath, dirnames, filenames) in os.walk(source_path):
1993 for filename in filenames:
1994 test_tree_filepath = os.path.join(dirpath, filename)
1995 common_path = os.path.commonprefix([test_tree_filepath,
1996 source_path])
1997 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1998 with open(test_tree_filepath, 'r') as f:
1999 content = f.read()
2000 files[relative_filepath] = content
2001 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002002 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002003
James E. Blaire18d4602017-01-05 11:17:28 -08002004 def assertNodepoolState(self):
2005 # Make sure that there are no pending requests
2006
2007 requests = self.fake_nodepool.getNodeRequests()
2008 self.assertEqual(len(requests), 0)
2009
2010 nodes = self.fake_nodepool.getNodes()
2011 for node in nodes:
2012 self.assertFalse(node['_lock'], "Node %s is locked" %
2013 (node['_oid'],))
2014
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002015 def assertNoGeneratedKeys(self):
2016 # Make sure that Zuul did not generate any project keys
2017 # (unless it was supposed to).
2018
2019 if self.create_project_keys:
2020 return
2021
2022 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2023 test_key = i.read()
2024
2025 key_root = os.path.join(self.state_root, 'keys')
2026 for root, dirname, files in os.walk(key_root):
2027 for fn in files:
2028 with open(os.path.join(root, fn)) as f:
2029 self.assertEqual(test_key, f.read())
2030
Clark Boylanb640e052014-04-03 16:41:46 -07002031 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002032 self.log.debug("Assert final state")
2033 # Make sure no jobs are running
2034 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002035 # Make sure that git.Repo objects have been garbage collected.
2036 repos = []
2037 gc.collect()
2038 for obj in gc.get_objects():
2039 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002040 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002041 repos.append(obj)
2042 self.assertEqual(len(repos), 0)
2043 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002044 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002045 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002046 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002047 for tenant in self.sched.abide.tenants.values():
2048 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002049 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002050 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002051
2052 def shutdown(self):
2053 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002054 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002055 self.merge_server.stop()
2056 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002057 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002058 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002059 self.sched.stop()
2060 self.sched.join()
2061 self.statsd.stop()
2062 self.statsd.join()
2063 self.webapp.stop()
2064 self.webapp.join()
2065 self.rpc.stop()
2066 self.rpc.join()
2067 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002068 self.fake_nodepool.stop()
2069 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002070 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002071 # we whitelist watchdog threads as they have relatively long delays
2072 # before noticing they should exit, but they should exit on their own.
2073 threads = [t for t in threading.enumerate()
2074 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002075 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002076 log_str = ""
2077 for thread_id, stack_frame in sys._current_frames().items():
2078 log_str += "Thread: %s\n" % thread_id
2079 log_str += "".join(traceback.format_stack(stack_frame))
2080 self.log.debug(log_str)
2081 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002082
James E. Blaira002b032017-04-18 10:35:48 -07002083 def assertCleanShutdown(self):
2084 pass
2085
James E. Blairc4ba97a2017-04-19 16:26:24 -07002086 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002087 parts = project.split('/')
2088 path = os.path.join(self.upstream_root, *parts[:-1])
2089 if not os.path.exists(path):
2090 os.makedirs(path)
2091 path = os.path.join(self.upstream_root, project)
2092 repo = git.Repo.init(path)
2093
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002094 with repo.config_writer() as config_writer:
2095 config_writer.set_value('user', 'email', 'user@example.com')
2096 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002097
Clark Boylanb640e052014-04-03 16:41:46 -07002098 repo.index.commit('initial commit')
2099 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002100 if tag:
2101 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002102
James E. Blair97d902e2014-08-21 13:25:56 -07002103 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002104 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002105 repo.git.clean('-x', '-f', '-d')
2106
James E. Blair97d902e2014-08-21 13:25:56 -07002107 def create_branch(self, project, branch):
2108 path = os.path.join(self.upstream_root, project)
2109 repo = git.Repo.init(path)
2110 fn = os.path.join(path, 'README')
2111
2112 branch_head = repo.create_head(branch)
2113 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002114 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002115 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002116 f.close()
2117 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002118 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002119
James E. Blair97d902e2014-08-21 13:25:56 -07002120 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002121 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002122 repo.git.clean('-x', '-f', '-d')
2123
Sachi King9f16d522016-03-16 12:20:45 +11002124 def create_commit(self, project):
2125 path = os.path.join(self.upstream_root, project)
2126 repo = git.Repo(path)
2127 repo.head.reference = repo.heads['master']
2128 file_name = os.path.join(path, 'README')
2129 with open(file_name, 'a') as f:
2130 f.write('creating fake commit\n')
2131 repo.index.add([file_name])
2132 commit = repo.index.commit('Creating a fake commit')
2133 return commit.hexsha
2134
James E. Blairf4a5f022017-04-18 14:01:10 -07002135 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002136 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002137 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002138 while len(self.builds):
2139 self.release(self.builds[0])
2140 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002141 i += 1
2142 if count is not None and i >= count:
2143 break
James E. Blairb8c16472015-05-05 14:55:26 -07002144
Clark Boylanb640e052014-04-03 16:41:46 -07002145 def release(self, job):
2146 if isinstance(job, FakeBuild):
2147 job.release()
2148 else:
2149 job.waiting = False
2150 self.log.debug("Queued job %s released" % job.unique)
2151 self.gearman_server.wakeConnections()
2152
2153 def getParameter(self, job, name):
2154 if isinstance(job, FakeBuild):
2155 return job.parameters[name]
2156 else:
2157 parameters = json.loads(job.arguments)
2158 return parameters[name]
2159
Clark Boylanb640e052014-04-03 16:41:46 -07002160 def haveAllBuildsReported(self):
2161 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002162 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002163 return False
2164 # Find out if every build that the worker has completed has been
2165 # reported back to Zuul. If it hasn't then that means a Gearman
2166 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002167 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002168 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002169 if not zbuild:
2170 # It has already been reported
2171 continue
2172 # It hasn't been reported yet.
2173 return False
2174 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002175 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002176 if connection.state == 'GRAB_WAIT':
2177 return False
2178 return True
2179
2180 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002181 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002182 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002183 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002184 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002185 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002186 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002187 for j in conn.related_jobs.values():
2188 if j.unique == build.uuid:
2189 client_job = j
2190 break
2191 if not client_job:
2192 self.log.debug("%s is not known to the gearman client" %
2193 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002194 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002195 if not client_job.handle:
2196 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002197 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002198 server_job = self.gearman_server.jobs.get(client_job.handle)
2199 if not server_job:
2200 self.log.debug("%s is not known to the gearman server" %
2201 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002202 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002203 if not hasattr(server_job, 'waiting'):
2204 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002205 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002206 if server_job.waiting:
2207 continue
James E. Blair17302972016-08-10 16:11:42 -07002208 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002209 self.log.debug("%s has not reported start" % build)
2210 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002211 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002212 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002213 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002214 if worker_build:
2215 if worker_build.isWaiting():
2216 continue
2217 else:
2218 self.log.debug("%s is running" % worker_build)
2219 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002220 else:
James E. Blair962220f2016-08-03 11:22:38 -07002221 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002222 return False
James E. Blaira002b032017-04-18 10:35:48 -07002223 for (build_uuid, job_worker) in \
2224 self.executor_server.job_workers.items():
2225 if build_uuid not in seen_builds:
2226 self.log.debug("%s is not finalized" % build_uuid)
2227 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002228 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002229
James E. Blairdce6cea2016-12-20 16:45:32 -08002230 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002231 if self.fake_nodepool.paused:
2232 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002233 if self.sched.nodepool.requests:
2234 return False
2235 return True
2236
Jan Hruban6b71aff2015-10-22 16:58:08 +02002237 def eventQueuesEmpty(self):
2238 for queue in self.event_queues:
2239 yield queue.empty()
2240
2241 def eventQueuesJoin(self):
2242 for queue in self.event_queues:
2243 queue.join()
2244
Clark Boylanb640e052014-04-03 16:41:46 -07002245 def waitUntilSettled(self):
2246 self.log.debug("Waiting until settled...")
2247 start = time.time()
2248 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002249 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002250 self.log.error("Timeout waiting for Zuul to settle")
2251 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002252 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002253 self.log.error(" %s: %s" % (queue, queue.empty()))
2254 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002255 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002256 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002257 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002258 self.log.error("All requests completed: %s" %
2259 (self.areAllNodeRequestsComplete(),))
2260 self.log.error("Merge client jobs: %s" %
2261 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002262 raise Exception("Timeout waiting for Zuul to settle")
2263 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002264
Paul Belanger174a8272017-03-14 13:20:10 -04002265 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002266 # have all build states propogated to zuul?
2267 if self.haveAllBuildsReported():
2268 # Join ensures that the queue is empty _and_ events have been
2269 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002270 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002271 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002272 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002273 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002274 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002275 self.areAllNodeRequestsComplete() and
2276 all(self.eventQueuesEmpty())):
2277 # The queue empty check is placed at the end to
2278 # ensure that if a component adds an event between
2279 # when locked the run handler and checked that the
2280 # components were stable, we don't erroneously
2281 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002282 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002283 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002284 self.log.debug("...settled.")
2285 return
2286 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002287 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002288 self.sched.wake_event.wait(0.1)
2289
2290 def countJobResults(self, jobs, result):
2291 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002292 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002293
James E. Blair96c6bf82016-01-15 16:20:40 -08002294 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002295 for job in self.history:
2296 if (job.name == name and
2297 (project is None or
2298 job.parameters['ZUUL_PROJECT'] == project)):
2299 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002300 raise Exception("Unable to find job %s in history" % name)
2301
2302 def assertEmptyQueues(self):
2303 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002304 for tenant in self.sched.abide.tenants.values():
2305 for pipeline in tenant.layout.pipelines.values():
2306 for queue in pipeline.queues:
2307 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002308 print('pipeline %s queue %s contents %s' % (
2309 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002310 self.assertEqual(len(queue.queue), 0,
2311 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002312
2313 def assertReportedStat(self, key, value=None, kind=None):
2314 start = time.time()
2315 while time.time() < (start + 5):
2316 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002317 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002318 if key == k:
2319 if value is None and kind is None:
2320 return
2321 elif value:
2322 if value == v:
2323 return
2324 elif kind:
2325 if v.endswith('|' + kind):
2326 return
2327 time.sleep(0.1)
2328
Clark Boylanb640e052014-04-03 16:41:46 -07002329 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002330
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002331 def assertBuilds(self, builds):
2332 """Assert that the running builds are as described.
2333
2334 The list of running builds is examined and must match exactly
2335 the list of builds described by the input.
2336
2337 :arg list builds: A list of dictionaries. Each item in the
2338 list must match the corresponding build in the build
2339 history, and each element of the dictionary must match the
2340 corresponding attribute of the build.
2341
2342 """
James E. Blair3158e282016-08-19 09:34:11 -07002343 try:
2344 self.assertEqual(len(self.builds), len(builds))
2345 for i, d in enumerate(builds):
2346 for k, v in d.items():
2347 self.assertEqual(
2348 getattr(self.builds[i], k), v,
2349 "Element %i in builds does not match" % (i,))
2350 except Exception:
2351 for build in self.builds:
2352 self.log.error("Running build: %s" % build)
2353 else:
2354 self.log.error("No running builds")
2355 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002356
James E. Blairb536ecc2016-08-31 10:11:42 -07002357 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002358 """Assert that the completed builds are as described.
2359
2360 The list of completed builds is examined and must match
2361 exactly the list of builds described by the input.
2362
2363 :arg list history: A list of dictionaries. Each item in the
2364 list must match the corresponding build in the build
2365 history, and each element of the dictionary must match the
2366 corresponding attribute of the build.
2367
James E. Blairb536ecc2016-08-31 10:11:42 -07002368 :arg bool ordered: If true, the history must match the order
2369 supplied, if false, the builds are permitted to have
2370 arrived in any order.
2371
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002372 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002373 def matches(history_item, item):
2374 for k, v in item.items():
2375 if getattr(history_item, k) != v:
2376 return False
2377 return True
James E. Blair3158e282016-08-19 09:34:11 -07002378 try:
2379 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002380 if ordered:
2381 for i, d in enumerate(history):
2382 if not matches(self.history[i], d):
2383 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002384 "Element %i in history does not match %s" %
2385 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002386 else:
2387 unseen = self.history[:]
2388 for i, d in enumerate(history):
2389 found = False
2390 for unseen_item in unseen:
2391 if matches(unseen_item, d):
2392 found = True
2393 unseen.remove(unseen_item)
2394 break
2395 if not found:
2396 raise Exception("No match found for element %i "
2397 "in history" % (i,))
2398 if unseen:
2399 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002400 except Exception:
2401 for build in self.history:
2402 self.log.error("Completed build: %s" % build)
2403 else:
2404 self.log.error("No completed builds")
2405 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002406
James E. Blair6ac368c2016-12-22 18:07:20 -08002407 def printHistory(self):
2408 """Log the build history.
2409
2410 This can be useful during tests to summarize what jobs have
2411 completed.
2412
2413 """
2414 self.log.debug("Build history:")
2415 for build in self.history:
2416 self.log.debug(build)
2417
James E. Blair59fdbac2015-12-07 17:08:06 -08002418 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002419 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2420
James E. Blair9ea70072017-04-19 16:05:30 -07002421 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002422 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002423 if not os.path.exists(root):
2424 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002425 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2426 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002427- tenant:
2428 name: openstack
2429 source:
2430 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002431 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002432 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002433 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002434 - org/project
2435 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002436 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002437 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002438 self.config.set('zuul', 'tenant_config',
2439 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002440 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002441
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002442 def addCommitToRepo(self, project, message, files,
2443 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002444 path = os.path.join(self.upstream_root, project)
2445 repo = git.Repo(path)
2446 repo.head.reference = branch
2447 zuul.merger.merger.reset_repo_to_head(repo)
2448 for fn, content in files.items():
2449 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002450 try:
2451 os.makedirs(os.path.dirname(fn))
2452 except OSError:
2453 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002454 with open(fn, 'w') as f:
2455 f.write(content)
2456 repo.index.add([fn])
2457 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002458 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002459 repo.heads[branch].commit = commit
2460 repo.head.reference = branch
2461 repo.git.clean('-x', '-f', '-d')
2462 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002463 if tag:
2464 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002465 return before
2466
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002467 def commitConfigUpdate(self, project_name, source_name):
2468 """Commit an update to zuul.yaml
2469
2470 This overwrites the zuul.yaml in the specificed project with
2471 the contents specified.
2472
2473 :arg str project_name: The name of the project containing
2474 zuul.yaml (e.g., common-config)
2475
2476 :arg str source_name: The path to the file (underneath the
2477 test fixture directory) whose contents should be used to
2478 replace zuul.yaml.
2479 """
2480
2481 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002482 files = {}
2483 with open(source_path, 'r') as f:
2484 data = f.read()
2485 layout = yaml.safe_load(data)
2486 files['zuul.yaml'] = data
2487 for item in layout:
2488 if 'job' in item:
2489 jobname = item['job']['name']
2490 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002491 before = self.addCommitToRepo(
2492 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002493 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002494 return before
2495
James E. Blair7fc8daa2016-08-08 15:37:15 -07002496 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002497
James E. Blair7fc8daa2016-08-08 15:37:15 -07002498 """Inject a Fake (Gerrit) event.
2499
2500 This method accepts a JSON-encoded event and simulates Zuul
2501 having received it from Gerrit. It could (and should)
2502 eventually apply to any connection type, but is currently only
2503 used with Gerrit connections. The name of the connection is
2504 used to look up the corresponding server, and the event is
2505 simulated as having been received by all Zuul connections
2506 attached to that server. So if two Gerrit connections in Zuul
2507 are connected to the same Gerrit server, and you invoke this
2508 method specifying the name of one of them, the event will be
2509 received by both.
2510
2511 .. note::
2512
2513 "self.fake_gerrit.addEvent" calls should be migrated to
2514 this method.
2515
2516 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002517 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002518 :arg str event: The JSON-encoded event.
2519
2520 """
2521 specified_conn = self.connections.connections[connection]
2522 for conn in self.connections.connections.values():
2523 if (isinstance(conn, specified_conn.__class__) and
2524 specified_conn.server == conn.server):
2525 conn.addEvent(event)
2526
James E. Blair3f876d52016-07-22 13:07:14 -07002527
2528class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002529 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002530 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002531
Joshua Heskethd78b4482015-09-14 16:56:34 -06002532
2533class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002534 def setup_config(self):
2535 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002536 for section_name in self.config.sections():
2537 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2538 section_name, re.I)
2539 if not con_match:
2540 continue
2541
2542 if self.config.get(section_name, 'driver') == 'sql':
2543 f = MySQLSchemaFixture()
2544 self.useFixture(f)
2545 if (self.config.get(section_name, 'dburi') ==
2546 '$MYSQL_FIXTURE_DBURI$'):
2547 self.config.set(section_name, 'dburi', f.dburi)