blob: b1ef3c9d88f384bbcf02ce86115f578d872df6d1 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
Clark Boylanb640e052014-04-03 16:41:46 -0700499 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 if 'submit' in action:
502 change.setMerged()
503 if message:
504 change.setReported()
505
506 def query(self, number):
507 change = self.changes.get(int(number))
508 if change:
509 return change.query()
510 return {}
511
James E. Blairc494d542014-08-06 09:23:52 -0700512 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700513 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700514 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800515 if query.startswith('change:'):
516 # Query a specific changeid
517 changeid = query[len('change:'):]
518 l = [change.query() for change in self.changes.values()
519 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700520 elif query.startswith('message:'):
521 # Query the content of a commit message
522 msg = query[len('message:'):].strip()
523 l = [change.query() for change in self.changes.values()
524 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800525 else:
526 # Query all open changes
527 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700528 return l
James E. Blairc494d542014-08-06 09:23:52 -0700529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700531 pass
532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def getGitUrl(self, project):
534 return os.path.join(self.upstream_root, project.name)
535
Clark Boylanb640e052014-04-03 16:41:46 -0700536
Gregory Haynes4fc12542015-04-22 20:38:06 -0700537class GithubChangeReference(git.Reference):
538 _common_path_default = "refs/pull"
539 _points_to_commits_only = True
540
541
542class FakeGithubPullRequest(object):
543
544 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100545 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700546 """Creates a new PR with several commits.
547 Sends an event about opened PR."""
548 self.github = github
549 self.source = github
550 self.number = number
551 self.project = project
552 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100553 self.subject = subject
554 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700555 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100556 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100558 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100559 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.updated_at = None
561 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100562 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100563 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100565 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700566 self._updateTimeStamp()
567
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
577
578 def getPullRequestOpenedEvent(self):
579 return self._getPullRequestEvent('opened')
580
581 def getPullRequestSynchronizeEvent(self):
582 return self._getPullRequestEvent('synchronize')
583
584 def getPullRequestReopenedEvent(self):
585 return self._getPullRequestEvent('reopened')
586
587 def getPullRequestClosedEvent(self):
588 return self._getPullRequestEvent('closed')
589
590 def addComment(self, message):
591 self.comments.append(message)
592 self._updateTimeStamp()
593
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200594 def getCommentAddedEvent(self, text):
595 name = 'issue_comment'
596 data = {
597 'action': 'created',
598 'issue': {
599 'number': self.number
600 },
601 'comment': {
602 'body': text
603 },
604 'repository': {
605 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100606 },
607 'sender': {
608 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200609 }
610 }
611 return (name, data)
612
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800613 def getReviewAddedEvent(self, review):
614 name = 'pull_request_review'
615 data = {
616 'action': 'submitted',
617 'pull_request': {
618 'number': self.number,
619 'title': self.subject,
620 'updated_at': self.updated_at,
621 'base': {
622 'ref': self.branch,
623 'repo': {
624 'full_name': self.project
625 }
626 },
627 'head': {
628 'sha': self.head_sha
629 }
630 },
631 'review': {
632 'state': review
633 },
634 'repository': {
635 'full_name': self.project
636 },
637 'sender': {
638 'login': 'ghuser'
639 }
640 }
641 return (name, data)
642
Jan Hruban16ad31f2015-11-07 14:39:07 +0100643 def addLabel(self, name):
644 if name not in self.labels:
645 self.labels.append(name)
646 self._updateTimeStamp()
647 return self._getLabelEvent(name)
648
649 def removeLabel(self, name):
650 if name in self.labels:
651 self.labels.remove(name)
652 self._updateTimeStamp()
653 return self._getUnlabelEvent(name)
654
655 def _getLabelEvent(self, label):
656 name = 'pull_request'
657 data = {
658 'action': 'labeled',
659 'pull_request': {
660 'number': self.number,
661 'updated_at': self.updated_at,
662 'base': {
663 'ref': self.branch,
664 'repo': {
665 'full_name': self.project
666 }
667 },
668 'head': {
669 'sha': self.head_sha
670 }
671 },
672 'label': {
673 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100674 },
675 'sender': {
676 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100677 }
678 }
679 return (name, data)
680
681 def _getUnlabelEvent(self, label):
682 name = 'pull_request'
683 data = {
684 'action': 'unlabeled',
685 'pull_request': {
686 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100687 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100688 'updated_at': self.updated_at,
689 'base': {
690 'ref': self.branch,
691 'repo': {
692 'full_name': self.project
693 }
694 },
695 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800696 'sha': self.head_sha,
697 'repo': {
698 'full_name': self.project
699 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100700 }
701 },
702 'label': {
703 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100704 },
705 'sender': {
706 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100707 }
708 }
709 return (name, data)
710
Gregory Haynes4fc12542015-04-22 20:38:06 -0700711 def _getRepo(self):
712 repo_path = os.path.join(self.upstream_root, self.project)
713 return git.Repo(repo_path)
714
715 def _createPRRef(self):
716 repo = self._getRepo()
717 GithubChangeReference.create(
718 repo, self._getPRReference(), 'refs/tags/init')
719
Jan Hruban570d01c2016-03-10 21:51:32 +0100720 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700721 repo = self._getRepo()
722 ref = repo.references[self._getPRReference()]
723 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100724 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100726 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700727 repo.head.reference = ref
728 zuul.merger.merger.reset_repo_to_head(repo)
729 repo.git.clean('-x', '-f', '-d')
730
Jan Hruban570d01c2016-03-10 21:51:32 +0100731 if files:
732 fn = files[0]
733 self.files = files
734 else:
735 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
736 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100737 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700738 fn = os.path.join(repo.working_dir, fn)
739 f = open(fn, 'w')
740 with open(fn, 'w') as f:
741 f.write("test %s %s\n" %
742 (self.branch, self.number))
743 repo.index.add([fn])
744
745 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800746 # Create an empty set of statuses for the given sha,
747 # each sha on a PR may have a status set on it
748 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700749 repo.head.reference = 'master'
750 zuul.merger.merger.reset_repo_to_head(repo)
751 repo.git.clean('-x', '-f', '-d')
752 repo.heads['master'].checkout()
753
754 def _updateTimeStamp(self):
755 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
756
757 def getPRHeadSha(self):
758 repo = self._getRepo()
759 return repo.references[self._getPRReference()].commit.hexsha
760
Jesse Keatingd96e5882017-01-19 13:55:50 -0800761 def setStatus(self, sha, state, url, description, context):
762 # Since we're bypassing github API, which would require a user, we
763 # hard set the user as 'zuul' here.
764 user = 'zuul'
765 # insert the status at the top of the list, to simulate that it
766 # is the most recent set status
767 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100768 'state': state,
769 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800770 'description': description,
771 'context': context,
772 'creator': {
773 'login': user
774 }
775 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100776
Gregory Haynes4fc12542015-04-22 20:38:06 -0700777 def _getPRReference(self):
778 return '%s/head' % self.number
779
780 def _getPullRequestEvent(self, action):
781 name = 'pull_request'
782 data = {
783 'action': action,
784 'number': self.number,
785 'pull_request': {
786 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100787 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700788 'updated_at': self.updated_at,
789 'base': {
790 'ref': self.branch,
791 'repo': {
792 'full_name': self.project
793 }
794 },
795 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800796 'sha': self.head_sha,
797 'repo': {
798 'full_name': self.project
799 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700800 }
Jan Hruban3b415922016-02-03 13:10:22 +0100801 },
802 'sender': {
803 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700804 }
805 }
806 return (name, data)
807
808
809class FakeGithubConnection(githubconnection.GithubConnection):
810 log = logging.getLogger("zuul.test.FakeGithubConnection")
811
812 def __init__(self, driver, connection_name, connection_config,
813 upstream_root=None):
814 super(FakeGithubConnection, self).__init__(driver, connection_name,
815 connection_config)
816 self.connection_name = connection_name
817 self.pr_number = 0
818 self.pull_requests = []
819 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100820 self.merge_failure = False
821 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700822
Jan Hruban570d01c2016-03-10 21:51:32 +0100823 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700824 self.pr_number += 1
825 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100826 self, self.pr_number, project, branch, subject, self.upstream_root,
827 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700828 self.pull_requests.append(pull_request)
829 return pull_request
830
Wayne1a78c612015-06-11 17:14:13 -0700831 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
832 if not old_rev:
833 old_rev = '00000000000000000000000000000000'
834 if not new_rev:
835 new_rev = random_sha1()
836 name = 'push'
837 data = {
838 'ref': ref,
839 'before': old_rev,
840 'after': new_rev,
841 'repository': {
842 'full_name': project
843 }
844 }
845 return (name, data)
846
Gregory Haynes4fc12542015-04-22 20:38:06 -0700847 def emitEvent(self, event):
848 """Emulates sending the GitHub webhook event to the connection."""
849 port = self.webapp.server.socket.getsockname()[1]
850 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700851 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700852 headers = {'X-Github-Event': name}
853 req = urllib.request.Request(
854 'http://localhost:%s/connection/%s/payload'
855 % (port, self.connection_name),
856 data=payload, headers=headers)
857 urllib.request.urlopen(req)
858
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200859 def getPull(self, project, number):
860 pr = self.pull_requests[number - 1]
861 data = {
862 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100863 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200864 'updated_at': pr.updated_at,
865 'base': {
866 'repo': {
867 'full_name': pr.project
868 },
869 'ref': pr.branch,
870 },
Jan Hruban37615e52015-11-19 14:30:49 +0100871 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200872 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800873 'sha': pr.head_sha,
874 'repo': {
875 'full_name': pr.project
876 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200877 }
878 }
879 return data
880
Jan Hruban570d01c2016-03-10 21:51:32 +0100881 def getPullFileNames(self, project, number):
882 pr = self.pull_requests[number - 1]
883 return pr.files
884
Jan Hruban3b415922016-02-03 13:10:22 +0100885 def getUser(self, login):
886 data = {
887 'username': login,
888 'name': 'Github User',
889 'email': 'github.user@example.com'
890 }
891 return data
892
Gregory Haynes4fc12542015-04-22 20:38:06 -0700893 def getGitUrl(self, project):
894 return os.path.join(self.upstream_root, str(project))
895
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200896 def real_getGitUrl(self, project):
897 return super(FakeGithubConnection, self).getGitUrl(project)
898
Gregory Haynes4fc12542015-04-22 20:38:06 -0700899 def getProjectBranches(self, project):
900 """Masks getProjectBranches since we don't have a real github"""
901
902 # just returns master for now
903 return ['master']
904
Jan Hrubane252a732017-01-03 15:03:09 +0100905 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700906 pull_request = self.pull_requests[pr_number - 1]
907 pull_request.addComment(message)
908
Jan Hruban3b415922016-02-03 13:10:22 +0100909 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100910 pull_request = self.pull_requests[pr_number - 1]
911 if self.merge_failure:
912 raise Exception('Pull request was not merged')
913 if self.merge_not_allowed_count > 0:
914 self.merge_not_allowed_count -= 1
915 raise MergeFailure('Merge was not successful due to mergeability'
916 ' conflict')
917 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100918 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100919
Jesse Keatingd96e5882017-01-19 13:55:50 -0800920 def getCommitStatuses(self, project, sha):
921 owner, proj = project.split('/')
922 for pr in self.pull_requests:
923 pr_owner, pr_project = pr.project.split('/')
924 if (pr_owner == owner and pr_project == proj and
925 pr.head_sha == sha):
926 return pr.statuses[sha]
927
Jan Hrubane252a732017-01-03 15:03:09 +0100928 def setCommitStatus(self, project, sha, state,
929 url='', description='', context=''):
930 owner, proj = project.split('/')
931 for pr in self.pull_requests:
932 pr_owner, pr_project = pr.project.split('/')
933 if (pr_owner == owner and pr_project == proj and
934 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800935 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +0100936
Jan Hruban16ad31f2015-11-07 14:39:07 +0100937 def labelPull(self, project, pr_number, label):
938 pull_request = self.pull_requests[pr_number - 1]
939 pull_request.addLabel(label)
940
941 def unlabelPull(self, project, pr_number, label):
942 pull_request = self.pull_requests[pr_number - 1]
943 pull_request.removeLabel(label)
944
Gregory Haynes4fc12542015-04-22 20:38:06 -0700945
Clark Boylanb640e052014-04-03 16:41:46 -0700946class BuildHistory(object):
947 def __init__(self, **kw):
948 self.__dict__.update(kw)
949
950 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700951 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
952 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700953
954
955class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200956 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700957 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700958 self.url = url
959
960 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700961 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700962 path = res.path
963 project = '/'.join(path.split('/')[2:-2])
964 ret = '001e# service=git-upload-pack\n'
965 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
966 'multi_ack thin-pack side-band side-band-64k ofs-delta '
967 'shallow no-progress include-tag multi_ack_detailed no-done\n')
968 path = os.path.join(self.upstream_root, project)
969 repo = git.Repo(path)
970 for ref in repo.refs:
971 r = ref.object.hexsha + ' ' + ref.path + '\n'
972 ret += '%04x%s' % (len(r) + 4, r)
973 ret += '0000'
974 return ret
975
976
Clark Boylanb640e052014-04-03 16:41:46 -0700977class FakeStatsd(threading.Thread):
978 def __init__(self):
979 threading.Thread.__init__(self)
980 self.daemon = True
981 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
982 self.sock.bind(('', 0))
983 self.port = self.sock.getsockname()[1]
984 self.wake_read, self.wake_write = os.pipe()
985 self.stats = []
986
987 def run(self):
988 while True:
989 poll = select.poll()
990 poll.register(self.sock, select.POLLIN)
991 poll.register(self.wake_read, select.POLLIN)
992 ret = poll.poll()
993 for (fd, event) in ret:
994 if fd == self.sock.fileno():
995 data = self.sock.recvfrom(1024)
996 if not data:
997 return
998 self.stats.append(data[0])
999 if fd == self.wake_read:
1000 return
1001
1002 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001003 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001004
1005
James E. Blaire1767bc2016-08-02 10:00:27 -07001006class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001007 log = logging.getLogger("zuul.test")
1008
Paul Belanger174a8272017-03-14 13:20:10 -04001009 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001010 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001011 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001012 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001013 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001014 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001015 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001016 # TODOv3(jeblair): self.node is really "the image of the node
1017 # assigned". We should rename it (self.node_image?) if we
1018 # keep using it like this, or we may end up exposing more of
1019 # the complexity around multi-node jobs here
1020 # (self.nodes[0].image?)
1021 self.node = None
1022 if len(self.parameters.get('nodes')) == 1:
1023 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001024 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001025 self.pipeline = self.parameters['ZUUL_PIPELINE']
1026 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001027 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001028 self.wait_condition = threading.Condition()
1029 self.waiting = False
1030 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001031 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001032 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001033 self.changes = None
1034 if 'ZUUL_CHANGE_IDS' in self.parameters:
1035 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001036
James E. Blair3158e282016-08-19 09:34:11 -07001037 def __repr__(self):
1038 waiting = ''
1039 if self.waiting:
1040 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001041 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1042 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001043
Clark Boylanb640e052014-04-03 16:41:46 -07001044 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001045 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001046 self.wait_condition.acquire()
1047 self.wait_condition.notify()
1048 self.waiting = False
1049 self.log.debug("Build %s released" % self.unique)
1050 self.wait_condition.release()
1051
1052 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001053 """Return whether this build is being held.
1054
1055 :returns: Whether the build is being held.
1056 :rtype: bool
1057 """
1058
Clark Boylanb640e052014-04-03 16:41:46 -07001059 self.wait_condition.acquire()
1060 if self.waiting:
1061 ret = True
1062 else:
1063 ret = False
1064 self.wait_condition.release()
1065 return ret
1066
1067 def _wait(self):
1068 self.wait_condition.acquire()
1069 self.waiting = True
1070 self.log.debug("Build %s waiting" % self.unique)
1071 self.wait_condition.wait()
1072 self.wait_condition.release()
1073
1074 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.log.debug('Running build %s' % self.unique)
1076
Paul Belanger174a8272017-03-14 13:20:10 -04001077 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001078 self.log.debug('Holding build %s' % self.unique)
1079 self._wait()
1080 self.log.debug("Build %s continuing" % self.unique)
1081
James E. Blair412fba82017-01-26 15:00:50 -08001082 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001083 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001084 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001085 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001086 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001087 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001088 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001089
James E. Blaire1767bc2016-08-02 10:00:27 -07001090 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001091
James E. Blaira5dba232016-08-08 15:53:24 -07001092 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001093 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001094 for change in changes:
1095 if self.hasChanges(change):
1096 return True
1097 return False
1098
James E. Blaire7b99a02016-08-05 14:27:34 -07001099 def hasChanges(self, *changes):
1100 """Return whether this build has certain changes in its git repos.
1101
1102 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001103 are expected to be present (in order) in the git repository of
1104 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001105
1106 :returns: Whether the build has the indicated changes.
1107 :rtype: bool
1108
1109 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001110 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001111 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001112 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001113 try:
1114 repo = git.Repo(path)
1115 except NoSuchPathError as e:
1116 self.log.debug('%s' % e)
1117 return False
1118 ref = self.parameters['ZUUL_REF']
1119 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1120 commit_message = '%s-1' % change.subject
1121 self.log.debug("Checking if build %s has changes; commit_message "
1122 "%s; repo_messages %s" % (self, commit_message,
1123 repo_messages))
1124 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001125 self.log.debug(" messages do not match")
1126 return False
1127 self.log.debug(" OK")
1128 return True
1129
Clark Boylanb640e052014-04-03 16:41:46 -07001130
Paul Belanger174a8272017-03-14 13:20:10 -04001131class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1132 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001133
Paul Belanger174a8272017-03-14 13:20:10 -04001134 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001135 they will report that they have started but then pause until
1136 released before reporting completion. This attribute may be
1137 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001138 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001139 be explicitly released.
1140
1141 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001142 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001143 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001144 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001145 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001146 self.hold_jobs_in_build = False
1147 self.lock = threading.Lock()
1148 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001149 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001150 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001151 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001152
James E. Blaira5dba232016-08-08 15:53:24 -07001153 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001154 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001155
1156 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001157 :arg Change change: The :py:class:`~tests.base.FakeChange`
1158 instance which should cause the job to fail. This job
1159 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001160
1161 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001162 l = self.fail_tests.get(name, [])
1163 l.append(change)
1164 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001165
James E. Blair962220f2016-08-03 11:22:38 -07001166 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001167 """Release a held build.
1168
1169 :arg str regex: A regular expression which, if supplied, will
1170 cause only builds with matching names to be released. If
1171 not supplied, all builds will be released.
1172
1173 """
James E. Blair962220f2016-08-03 11:22:38 -07001174 builds = self.running_builds[:]
1175 self.log.debug("Releasing build %s (%s)" % (regex,
1176 len(self.running_builds)))
1177 for build in builds:
1178 if not regex or re.match(regex, build.name):
1179 self.log.debug("Releasing build %s" %
1180 (build.parameters['ZUUL_UUID']))
1181 build.release()
1182 else:
1183 self.log.debug("Not releasing build %s" %
1184 (build.parameters['ZUUL_UUID']))
1185 self.log.debug("Done releasing builds %s (%s)" %
1186 (regex, len(self.running_builds)))
1187
Paul Belanger174a8272017-03-14 13:20:10 -04001188 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001189 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001190 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001191 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001192 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001193 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001194 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001195 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001196 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1197 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001198
1199 def stopJob(self, job):
1200 self.log.debug("handle stop")
1201 parameters = json.loads(job.arguments)
1202 uuid = parameters['uuid']
1203 for build in self.running_builds:
1204 if build.unique == uuid:
1205 build.aborted = True
1206 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001207 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001208
James E. Blaira002b032017-04-18 10:35:48 -07001209 def stop(self):
1210 for build in self.running_builds:
1211 build.release()
1212 super(RecordingExecutorServer, self).stop()
1213
Joshua Hesketh50c21782016-10-13 21:34:14 +11001214
Paul Belanger174a8272017-03-14 13:20:10 -04001215class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001216 def doMergeChanges(self, items):
1217 # Get a merger in order to update the repos involved in this job.
1218 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1219 if not commit: # merge conflict
1220 self.recordResult('MERGER_FAILURE')
1221 return commit
1222
1223 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001224 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001225 self.executor_server.lock.acquire()
1226 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001227 BuildHistory(name=build.name, result=result, changes=build.changes,
1228 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001229 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001230 pipeline=build.parameters['ZUUL_PIPELINE'])
1231 )
Paul Belanger174a8272017-03-14 13:20:10 -04001232 self.executor_server.running_builds.remove(build)
1233 del self.executor_server.job_builds[self.job.unique]
1234 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001235
1236 def runPlaybooks(self, args):
1237 build = self.executor_server.job_builds[self.job.unique]
1238 build.jobdir = self.jobdir
1239
1240 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1241 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001242 return result
1243
Monty Taylore6562aa2017-02-20 07:37:39 -05001244 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001245 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001246
Paul Belanger174a8272017-03-14 13:20:10 -04001247 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001248 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001249 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001250 else:
1251 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001252 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001253
James E. Blairad8dca02017-02-21 11:48:32 -05001254 def getHostList(self, args):
1255 self.log.debug("hostlist")
1256 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001257 for host in hosts:
1258 host['host_vars']['ansible_connection'] = 'local'
1259
1260 hosts.append(dict(
1261 name='localhost',
1262 host_vars=dict(ansible_connection='local'),
1263 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001264 return hosts
1265
James E. Blairf5dbd002015-12-23 15:26:17 -08001266
Clark Boylanb640e052014-04-03 16:41:46 -07001267class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001268 """A Gearman server for use in tests.
1269
1270 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1271 added to the queue but will not be distributed to workers
1272 until released. This attribute may be changed at any time and
1273 will take effect for subsequently enqueued jobs, but
1274 previously held jobs will still need to be explicitly
1275 released.
1276
1277 """
1278
Clark Boylanb640e052014-04-03 16:41:46 -07001279 def __init__(self):
1280 self.hold_jobs_in_queue = False
1281 super(FakeGearmanServer, self).__init__(0)
1282
1283 def getJobForConnection(self, connection, peek=False):
1284 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1285 for job in queue:
1286 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001287 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001288 job.waiting = self.hold_jobs_in_queue
1289 else:
1290 job.waiting = False
1291 if job.waiting:
1292 continue
1293 if job.name in connection.functions:
1294 if not peek:
1295 queue.remove(job)
1296 connection.related_jobs[job.handle] = job
1297 job.worker_connection = connection
1298 job.running = True
1299 return job
1300 return None
1301
1302 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001303 """Release a held job.
1304
1305 :arg str regex: A regular expression which, if supplied, will
1306 cause only jobs with matching names to be released. If
1307 not supplied, all jobs will be released.
1308 """
Clark Boylanb640e052014-04-03 16:41:46 -07001309 released = False
1310 qlen = (len(self.high_queue) + len(self.normal_queue) +
1311 len(self.low_queue))
1312 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1313 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001314 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001315 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001316 parameters = json.loads(job.arguments)
1317 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001318 self.log.debug("releasing queued job %s" %
1319 job.unique)
1320 job.waiting = False
1321 released = True
1322 else:
1323 self.log.debug("not releasing queued job %s" %
1324 job.unique)
1325 if released:
1326 self.wakeConnections()
1327 qlen = (len(self.high_queue) + len(self.normal_queue) +
1328 len(self.low_queue))
1329 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1330
1331
1332class FakeSMTP(object):
1333 log = logging.getLogger('zuul.FakeSMTP')
1334
1335 def __init__(self, messages, server, port):
1336 self.server = server
1337 self.port = port
1338 self.messages = messages
1339
1340 def sendmail(self, from_email, to_email, msg):
1341 self.log.info("Sending email from %s, to %s, with msg %s" % (
1342 from_email, to_email, msg))
1343
1344 headers = msg.split('\n\n', 1)[0]
1345 body = msg.split('\n\n', 1)[1]
1346
1347 self.messages.append(dict(
1348 from_email=from_email,
1349 to_email=to_email,
1350 msg=msg,
1351 headers=headers,
1352 body=body,
1353 ))
1354
1355 return True
1356
1357 def quit(self):
1358 return True
1359
1360
James E. Blairdce6cea2016-12-20 16:45:32 -08001361class FakeNodepool(object):
1362 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001363 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001364
1365 log = logging.getLogger("zuul.test.FakeNodepool")
1366
1367 def __init__(self, host, port, chroot):
1368 self.client = kazoo.client.KazooClient(
1369 hosts='%s:%s%s' % (host, port, chroot))
1370 self.client.start()
1371 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001372 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001373 self.thread = threading.Thread(target=self.run)
1374 self.thread.daemon = True
1375 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001376 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001377
1378 def stop(self):
1379 self._running = False
1380 self.thread.join()
1381 self.client.stop()
1382 self.client.close()
1383
1384 def run(self):
1385 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001386 try:
1387 self._run()
1388 except Exception:
1389 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001390 time.sleep(0.1)
1391
1392 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001393 if self.paused:
1394 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001395 for req in self.getNodeRequests():
1396 self.fulfillRequest(req)
1397
1398 def getNodeRequests(self):
1399 try:
1400 reqids = self.client.get_children(self.REQUEST_ROOT)
1401 except kazoo.exceptions.NoNodeError:
1402 return []
1403 reqs = []
1404 for oid in sorted(reqids):
1405 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001406 try:
1407 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001408 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001409 data['_oid'] = oid
1410 reqs.append(data)
1411 except kazoo.exceptions.NoNodeError:
1412 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001413 return reqs
1414
James E. Blaire18d4602017-01-05 11:17:28 -08001415 def getNodes(self):
1416 try:
1417 nodeids = self.client.get_children(self.NODE_ROOT)
1418 except kazoo.exceptions.NoNodeError:
1419 return []
1420 nodes = []
1421 for oid in sorted(nodeids):
1422 path = self.NODE_ROOT + '/' + oid
1423 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001424 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001425 data['_oid'] = oid
1426 try:
1427 lockfiles = self.client.get_children(path + '/lock')
1428 except kazoo.exceptions.NoNodeError:
1429 lockfiles = []
1430 if lockfiles:
1431 data['_lock'] = True
1432 else:
1433 data['_lock'] = False
1434 nodes.append(data)
1435 return nodes
1436
James E. Blaira38c28e2017-01-04 10:33:20 -08001437 def makeNode(self, request_id, node_type):
1438 now = time.time()
1439 path = '/nodepool/nodes/'
1440 data = dict(type=node_type,
1441 provider='test-provider',
1442 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001443 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001444 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001445 public_ipv4='127.0.0.1',
1446 private_ipv4=None,
1447 public_ipv6=None,
1448 allocated_to=request_id,
1449 state='ready',
1450 state_time=now,
1451 created_time=now,
1452 updated_time=now,
1453 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001454 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001455 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001456 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001457 path = self.client.create(path, data,
1458 makepath=True,
1459 sequence=True)
1460 nodeid = path.split("/")[-1]
1461 return nodeid
1462
James E. Blair6ab79e02017-01-06 10:10:17 -08001463 def addFailRequest(self, request):
1464 self.fail_requests.add(request['_oid'])
1465
James E. Blairdce6cea2016-12-20 16:45:32 -08001466 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001467 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001468 return
1469 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001470 oid = request['_oid']
1471 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001472
James E. Blair6ab79e02017-01-06 10:10:17 -08001473 if oid in self.fail_requests:
1474 request['state'] = 'failed'
1475 else:
1476 request['state'] = 'fulfilled'
1477 nodes = []
1478 for node in request['node_types']:
1479 nodeid = self.makeNode(oid, node)
1480 nodes.append(nodeid)
1481 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001482
James E. Blaira38c28e2017-01-04 10:33:20 -08001483 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001484 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001485 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001486 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001487 try:
1488 self.client.set(path, data)
1489 except kazoo.exceptions.NoNodeError:
1490 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001491
1492
James E. Blair498059b2016-12-20 13:50:13 -08001493class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001494 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001495 super(ChrootedKazooFixture, self).__init__()
1496
1497 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1498 if ':' in zk_host:
1499 host, port = zk_host.split(':')
1500 else:
1501 host = zk_host
1502 port = None
1503
1504 self.zookeeper_host = host
1505
1506 if not port:
1507 self.zookeeper_port = 2181
1508 else:
1509 self.zookeeper_port = int(port)
1510
Clark Boylan621ec9a2017-04-07 17:41:33 -07001511 self.test_id = test_id
1512
James E. Blair498059b2016-12-20 13:50:13 -08001513 def _setUp(self):
1514 # Make sure the test chroot paths do not conflict
1515 random_bits = ''.join(random.choice(string.ascii_lowercase +
1516 string.ascii_uppercase)
1517 for x in range(8))
1518
Clark Boylan621ec9a2017-04-07 17:41:33 -07001519 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001520 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1521
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001522 self.addCleanup(self._cleanup)
1523
James E. Blair498059b2016-12-20 13:50:13 -08001524 # Ensure the chroot path exists and clean up any pre-existing znodes.
1525 _tmp_client = kazoo.client.KazooClient(
1526 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1527 _tmp_client.start()
1528
1529 if _tmp_client.exists(self.zookeeper_chroot):
1530 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1531
1532 _tmp_client.ensure_path(self.zookeeper_chroot)
1533 _tmp_client.stop()
1534 _tmp_client.close()
1535
James E. Blair498059b2016-12-20 13:50:13 -08001536 def _cleanup(self):
1537 '''Remove the chroot path.'''
1538 # Need a non-chroot'ed client to remove the chroot path
1539 _tmp_client = kazoo.client.KazooClient(
1540 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1541 _tmp_client.start()
1542 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1543 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001544 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001545
1546
Joshua Heskethd78b4482015-09-14 16:56:34 -06001547class MySQLSchemaFixture(fixtures.Fixture):
1548 def setUp(self):
1549 super(MySQLSchemaFixture, self).setUp()
1550
1551 random_bits = ''.join(random.choice(string.ascii_lowercase +
1552 string.ascii_uppercase)
1553 for x in range(8))
1554 self.name = '%s_%s' % (random_bits, os.getpid())
1555 self.passwd = uuid.uuid4().hex
1556 db = pymysql.connect(host="localhost",
1557 user="openstack_citest",
1558 passwd="openstack_citest",
1559 db="openstack_citest")
1560 cur = db.cursor()
1561 cur.execute("create database %s" % self.name)
1562 cur.execute(
1563 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1564 (self.name, self.name, self.passwd))
1565 cur.execute("flush privileges")
1566
1567 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1568 self.passwd,
1569 self.name)
1570 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1571 self.addCleanup(self.cleanup)
1572
1573 def cleanup(self):
1574 db = pymysql.connect(host="localhost",
1575 user="openstack_citest",
1576 passwd="openstack_citest",
1577 db="openstack_citest")
1578 cur = db.cursor()
1579 cur.execute("drop database %s" % self.name)
1580 cur.execute("drop user '%s'@'localhost'" % self.name)
1581 cur.execute("flush privileges")
1582
1583
Maru Newby3fe5f852015-01-13 04:22:14 +00001584class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001585 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001586 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001587
James E. Blair1c236df2017-02-01 14:07:24 -08001588 def attachLogs(self, *args):
1589 def reader():
1590 self._log_stream.seek(0)
1591 while True:
1592 x = self._log_stream.read(4096)
1593 if not x:
1594 break
1595 yield x.encode('utf8')
1596 content = testtools.content.content_from_reader(
1597 reader,
1598 testtools.content_type.UTF8_TEXT,
1599 False)
1600 self.addDetail('logging', content)
1601
Clark Boylanb640e052014-04-03 16:41:46 -07001602 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001603 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001604 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1605 try:
1606 test_timeout = int(test_timeout)
1607 except ValueError:
1608 # If timeout value is invalid do not set a timeout.
1609 test_timeout = 0
1610 if test_timeout > 0:
1611 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1612
1613 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1614 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1615 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1616 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1617 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1618 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1619 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1620 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1621 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1622 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001623 self._log_stream = StringIO()
1624 self.addOnException(self.attachLogs)
1625 else:
1626 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001627
James E. Blair1c236df2017-02-01 14:07:24 -08001628 handler = logging.StreamHandler(self._log_stream)
1629 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1630 '%(levelname)-8s %(message)s')
1631 handler.setFormatter(formatter)
1632
1633 logger = logging.getLogger()
1634 logger.setLevel(logging.DEBUG)
1635 logger.addHandler(handler)
1636
Clark Boylan3410d532017-04-25 12:35:29 -07001637 # Make sure we don't carry old handlers around in process state
1638 # which slows down test runs
1639 self.addCleanup(logger.removeHandler, handler)
1640 self.addCleanup(handler.close)
1641 self.addCleanup(handler.flush)
1642
James E. Blair1c236df2017-02-01 14:07:24 -08001643 # NOTE(notmorgan): Extract logging overrides for specific
1644 # libraries from the OS_LOG_DEFAULTS env and create loggers
1645 # for each. This is used to limit the output during test runs
1646 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001647 log_defaults_from_env = os.environ.get(
1648 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001649 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001650
James E. Blairdce6cea2016-12-20 16:45:32 -08001651 if log_defaults_from_env:
1652 for default in log_defaults_from_env.split(','):
1653 try:
1654 name, level_str = default.split('=', 1)
1655 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001656 logger = logging.getLogger(name)
1657 logger.setLevel(level)
1658 logger.addHandler(handler)
1659 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001660 except ValueError:
1661 # NOTE(notmorgan): Invalid format of the log default,
1662 # skip and don't try and apply a logger for the
1663 # specified module
1664 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001665
Maru Newby3fe5f852015-01-13 04:22:14 +00001666
1667class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001668 """A test case with a functioning Zuul.
1669
1670 The following class variables are used during test setup and can
1671 be overidden by subclasses but are effectively read-only once a
1672 test method starts running:
1673
1674 :cvar str config_file: This points to the main zuul config file
1675 within the fixtures directory. Subclasses may override this
1676 to obtain a different behavior.
1677
1678 :cvar str tenant_config_file: This is the tenant config file
1679 (which specifies from what git repos the configuration should
1680 be loaded). It defaults to the value specified in
1681 `config_file` but can be overidden by subclasses to obtain a
1682 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001683 configuration. See also the :py:func:`simple_layout`
1684 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001685
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001686 :cvar bool create_project_keys: Indicates whether Zuul should
1687 auto-generate keys for each project, or whether the test
1688 infrastructure should insert dummy keys to save time during
1689 startup. Defaults to False.
1690
James E. Blaire7b99a02016-08-05 14:27:34 -07001691 The following are instance variables that are useful within test
1692 methods:
1693
1694 :ivar FakeGerritConnection fake_<connection>:
1695 A :py:class:`~tests.base.FakeGerritConnection` will be
1696 instantiated for each connection present in the config file
1697 and stored here. For instance, `fake_gerrit` will hold the
1698 FakeGerritConnection object for a connection named `gerrit`.
1699
1700 :ivar FakeGearmanServer gearman_server: An instance of
1701 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1702 server that all of the Zuul components in this test use to
1703 communicate with each other.
1704
Paul Belanger174a8272017-03-14 13:20:10 -04001705 :ivar RecordingExecutorServer executor_server: An instance of
1706 :py:class:`~tests.base.RecordingExecutorServer` which is the
1707 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001708
1709 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1710 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001711 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001712 list upon completion.
1713
1714 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1715 objects representing completed builds. They are appended to
1716 the list in the order they complete.
1717
1718 """
1719
James E. Blair83005782015-12-11 14:46:03 -08001720 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001721 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001722 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001723
1724 def _startMerger(self):
1725 self.merge_server = zuul.merger.server.MergeServer(self.config,
1726 self.connections)
1727 self.merge_server.start()
1728
Maru Newby3fe5f852015-01-13 04:22:14 +00001729 def setUp(self):
1730 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001731
1732 self.setupZK()
1733
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001734 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001735 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001736 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1737 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001738 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001739 tmp_root = tempfile.mkdtemp(
1740 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001741 self.test_root = os.path.join(tmp_root, "zuul-test")
1742 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001743 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001744 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001745 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001746
1747 if os.path.exists(self.test_root):
1748 shutil.rmtree(self.test_root)
1749 os.makedirs(self.test_root)
1750 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001751 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001752
1753 # Make per test copy of Configuration.
1754 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001755 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001756 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001757 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001758 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001759 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001760 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001761
Clark Boylanb640e052014-04-03 16:41:46 -07001762 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001763 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1764 # see: https://github.com/jsocol/pystatsd/issues/61
1765 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001766 os.environ['STATSD_PORT'] = str(self.statsd.port)
1767 self.statsd.start()
1768 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001769 reload_module(statsd)
1770 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001771
1772 self.gearman_server = FakeGearmanServer()
1773
1774 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001775 self.log.info("Gearman server on port %s" %
1776 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001777
James E. Blaire511d2f2016-12-08 15:22:26 -08001778 gerritsource.GerritSource.replication_timeout = 1.5
1779 gerritsource.GerritSource.replication_retry_interval = 0.5
1780 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001781
Joshua Hesketh352264b2015-08-11 23:42:08 +10001782 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001783
Jan Hruban7083edd2015-08-21 14:00:54 +02001784 self.webapp = zuul.webapp.WebApp(
1785 self.sched, port=0, listen_address='127.0.0.1')
1786
Jan Hruban6b71aff2015-10-22 16:58:08 +02001787 self.event_queues = [
1788 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001789 self.sched.trigger_event_queue,
1790 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001791 ]
1792
James E. Blairfef78942016-03-11 16:28:56 -08001793 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001794 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001795
Clark Boylanb640e052014-04-03 16:41:46 -07001796 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001797 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001798 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001799 return FakeURLOpener(self.upstream_root, *args, **kw)
1800
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001801 old_urlopen = urllib.request.urlopen
1802 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001803
James E. Blair3f876d52016-07-22 13:07:14 -07001804 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001805
Paul Belanger174a8272017-03-14 13:20:10 -04001806 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001807 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001808 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001809 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001810 _test_root=self.test_root,
1811 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001812 self.executor_server.start()
1813 self.history = self.executor_server.build_history
1814 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001815
Paul Belanger174a8272017-03-14 13:20:10 -04001816 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001817 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001818 self.merge_client = zuul.merger.client.MergeClient(
1819 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001820 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001821 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001822 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001823
James E. Blair0d5a36e2017-02-21 10:53:44 -05001824 self.fake_nodepool = FakeNodepool(
1825 self.zk_chroot_fixture.zookeeper_host,
1826 self.zk_chroot_fixture.zookeeper_port,
1827 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001828
Paul Belanger174a8272017-03-14 13:20:10 -04001829 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001830 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001831 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001832 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001833
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001834 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001835
1836 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001837 self.webapp.start()
1838 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001839 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001840 # Cleanups are run in reverse order
1841 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001842 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001843 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001844
James E. Blairb9c0d772017-03-03 14:34:49 -08001845 self.sched.reconfigure(self.config)
1846 self.sched.resume()
1847
James E. Blairfef78942016-03-11 16:28:56 -08001848 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001849 # Set up gerrit related fakes
1850 # Set a changes database so multiple FakeGerrit's can report back to
1851 # a virtual canonical database given by the configured hostname
1852 self.gerrit_changes_dbs = {}
1853
1854 def getGerritConnection(driver, name, config):
1855 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1856 con = FakeGerritConnection(driver, name, config,
1857 changes_db=db,
1858 upstream_root=self.upstream_root)
1859 self.event_queues.append(con.event_queue)
1860 setattr(self, 'fake_' + name, con)
1861 return con
1862
1863 self.useFixture(fixtures.MonkeyPatch(
1864 'zuul.driver.gerrit.GerritDriver.getConnection',
1865 getGerritConnection))
1866
Gregory Haynes4fc12542015-04-22 20:38:06 -07001867 def getGithubConnection(driver, name, config):
1868 con = FakeGithubConnection(driver, name, config,
1869 upstream_root=self.upstream_root)
1870 setattr(self, 'fake_' + name, con)
1871 return con
1872
1873 self.useFixture(fixtures.MonkeyPatch(
1874 'zuul.driver.github.GithubDriver.getConnection',
1875 getGithubConnection))
1876
James E. Blaire511d2f2016-12-08 15:22:26 -08001877 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001878 # TODO(jhesketh): This should come from lib.connections for better
1879 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001880 # Register connections from the config
1881 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001882
Joshua Hesketh352264b2015-08-11 23:42:08 +10001883 def FakeSMTPFactory(*args, **kw):
1884 args = [self.smtp_messages] + list(args)
1885 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001886
Joshua Hesketh352264b2015-08-11 23:42:08 +10001887 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001888
James E. Blaire511d2f2016-12-08 15:22:26 -08001889 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001890 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001891 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001892
James E. Blair83005782015-12-11 14:46:03 -08001893 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001894 # This creates the per-test configuration object. It can be
1895 # overriden by subclasses, but should not need to be since it
1896 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001897 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001898 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001899
1900 if not self.setupSimpleLayout():
1901 if hasattr(self, 'tenant_config_file'):
1902 self.config.set('zuul', 'tenant_config',
1903 self.tenant_config_file)
1904 git_path = os.path.join(
1905 os.path.dirname(
1906 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1907 'git')
1908 if os.path.exists(git_path):
1909 for reponame in os.listdir(git_path):
1910 project = reponame.replace('_', '/')
1911 self.copyDirToRepo(project,
1912 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001913 self.setupAllProjectKeys()
1914
James E. Blair06cc3922017-04-19 10:08:10 -07001915 def setupSimpleLayout(self):
1916 # If the test method has been decorated with a simple_layout,
1917 # use that instead of the class tenant_config_file. Set up a
1918 # single config-project with the specified layout, and
1919 # initialize repos for all of the 'project' entries which
1920 # appear in the layout.
1921 test_name = self.id().split('.')[-1]
1922 test = getattr(self, test_name)
1923 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001924 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001925 else:
1926 return False
1927
James E. Blairb70e55a2017-04-19 12:57:02 -07001928 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001929 path = os.path.join(FIXTURE_DIR, path)
1930 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001931 data = f.read()
1932 layout = yaml.safe_load(data)
1933 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001934 untrusted_projects = []
1935 for item in layout:
1936 if 'project' in item:
1937 name = item['project']['name']
1938 untrusted_projects.append(name)
1939 self.init_repo(name)
1940 self.addCommitToRepo(name, 'initial commit',
1941 files={'README': ''},
1942 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001943 if 'job' in item:
1944 jobname = item['job']['name']
1945 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001946
1947 root = os.path.join(self.test_root, "config")
1948 if not os.path.exists(root):
1949 os.makedirs(root)
1950 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1951 config = [{'tenant':
1952 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001953 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001954 {'config-projects': ['common-config'],
1955 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001956 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001957 f.close()
1958 self.config.set('zuul', 'tenant_config',
1959 os.path.join(FIXTURE_DIR, f.name))
1960
1961 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001962 self.addCommitToRepo('common-config', 'add content from fixture',
1963 files, branch='master', tag='init')
1964
1965 return True
1966
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001967 def setupAllProjectKeys(self):
1968 if self.create_project_keys:
1969 return
1970
1971 path = self.config.get('zuul', 'tenant_config')
1972 with open(os.path.join(FIXTURE_DIR, path)) as f:
1973 tenant_config = yaml.safe_load(f.read())
1974 for tenant in tenant_config:
1975 sources = tenant['tenant']['source']
1976 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001977 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001978 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001979 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001980 self.setupProjectKeys(source, project)
1981
1982 def setupProjectKeys(self, source, project):
1983 # Make sure we set up an RSA key for the project so that we
1984 # don't spend time generating one:
1985
1986 key_root = os.path.join(self.state_root, 'keys')
1987 if not os.path.isdir(key_root):
1988 os.mkdir(key_root, 0o700)
1989 private_key_file = os.path.join(key_root, source, project + '.pem')
1990 private_key_dir = os.path.dirname(private_key_file)
1991 self.log.debug("Installing test keys for project %s at %s" % (
1992 project, private_key_file))
1993 if not os.path.isdir(private_key_dir):
1994 os.makedirs(private_key_dir)
1995 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1996 with open(private_key_file, 'w') as o:
1997 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001998
James E. Blair498059b2016-12-20 13:50:13 -08001999 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002000 self.zk_chroot_fixture = self.useFixture(
2001 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002002 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002003 self.zk_chroot_fixture.zookeeper_host,
2004 self.zk_chroot_fixture.zookeeper_port,
2005 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002006
James E. Blair96c6bf82016-01-15 16:20:40 -08002007 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002008 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002009
2010 files = {}
2011 for (dirpath, dirnames, filenames) in os.walk(source_path):
2012 for filename in filenames:
2013 test_tree_filepath = os.path.join(dirpath, filename)
2014 common_path = os.path.commonprefix([test_tree_filepath,
2015 source_path])
2016 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2017 with open(test_tree_filepath, 'r') as f:
2018 content = f.read()
2019 files[relative_filepath] = content
2020 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002021 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002022
James E. Blaire18d4602017-01-05 11:17:28 -08002023 def assertNodepoolState(self):
2024 # Make sure that there are no pending requests
2025
2026 requests = self.fake_nodepool.getNodeRequests()
2027 self.assertEqual(len(requests), 0)
2028
2029 nodes = self.fake_nodepool.getNodes()
2030 for node in nodes:
2031 self.assertFalse(node['_lock'], "Node %s is locked" %
2032 (node['_oid'],))
2033
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002034 def assertNoGeneratedKeys(self):
2035 # Make sure that Zuul did not generate any project keys
2036 # (unless it was supposed to).
2037
2038 if self.create_project_keys:
2039 return
2040
2041 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2042 test_key = i.read()
2043
2044 key_root = os.path.join(self.state_root, 'keys')
2045 for root, dirname, files in os.walk(key_root):
2046 for fn in files:
2047 with open(os.path.join(root, fn)) as f:
2048 self.assertEqual(test_key, f.read())
2049
Clark Boylanb640e052014-04-03 16:41:46 -07002050 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002051 self.log.debug("Assert final state")
2052 # Make sure no jobs are running
2053 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002054 # Make sure that git.Repo objects have been garbage collected.
2055 repos = []
2056 gc.collect()
2057 for obj in gc.get_objects():
2058 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002059 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002060 repos.append(obj)
2061 self.assertEqual(len(repos), 0)
2062 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002063 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002064 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002065 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002066 for tenant in self.sched.abide.tenants.values():
2067 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002068 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002069 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002070
2071 def shutdown(self):
2072 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002073 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002074 self.merge_server.stop()
2075 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002076 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002077 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002078 self.sched.stop()
2079 self.sched.join()
2080 self.statsd.stop()
2081 self.statsd.join()
2082 self.webapp.stop()
2083 self.webapp.join()
2084 self.rpc.stop()
2085 self.rpc.join()
2086 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002087 self.fake_nodepool.stop()
2088 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002089 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002090 # we whitelist watchdog threads as they have relatively long delays
2091 # before noticing they should exit, but they should exit on their own.
2092 threads = [t for t in threading.enumerate()
2093 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002094 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002095 log_str = ""
2096 for thread_id, stack_frame in sys._current_frames().items():
2097 log_str += "Thread: %s\n" % thread_id
2098 log_str += "".join(traceback.format_stack(stack_frame))
2099 self.log.debug(log_str)
2100 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002101
James E. Blaira002b032017-04-18 10:35:48 -07002102 def assertCleanShutdown(self):
2103 pass
2104
James E. Blairc4ba97a2017-04-19 16:26:24 -07002105 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002106 parts = project.split('/')
2107 path = os.path.join(self.upstream_root, *parts[:-1])
2108 if not os.path.exists(path):
2109 os.makedirs(path)
2110 path = os.path.join(self.upstream_root, project)
2111 repo = git.Repo.init(path)
2112
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002113 with repo.config_writer() as config_writer:
2114 config_writer.set_value('user', 'email', 'user@example.com')
2115 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002116
Clark Boylanb640e052014-04-03 16:41:46 -07002117 repo.index.commit('initial commit')
2118 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002119 if tag:
2120 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002121
James E. Blair97d902e2014-08-21 13:25:56 -07002122 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002123 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002124 repo.git.clean('-x', '-f', '-d')
2125
James E. Blair97d902e2014-08-21 13:25:56 -07002126 def create_branch(self, project, branch):
2127 path = os.path.join(self.upstream_root, project)
2128 repo = git.Repo.init(path)
2129 fn = os.path.join(path, 'README')
2130
2131 branch_head = repo.create_head(branch)
2132 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002133 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002134 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002135 f.close()
2136 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002137 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002138
James E. Blair97d902e2014-08-21 13:25:56 -07002139 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002140 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002141 repo.git.clean('-x', '-f', '-d')
2142
Sachi King9f16d522016-03-16 12:20:45 +11002143 def create_commit(self, project):
2144 path = os.path.join(self.upstream_root, project)
2145 repo = git.Repo(path)
2146 repo.head.reference = repo.heads['master']
2147 file_name = os.path.join(path, 'README')
2148 with open(file_name, 'a') as f:
2149 f.write('creating fake commit\n')
2150 repo.index.add([file_name])
2151 commit = repo.index.commit('Creating a fake commit')
2152 return commit.hexsha
2153
James E. Blairf4a5f022017-04-18 14:01:10 -07002154 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002155 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002156 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002157 while len(self.builds):
2158 self.release(self.builds[0])
2159 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002160 i += 1
2161 if count is not None and i >= count:
2162 break
James E. Blairb8c16472015-05-05 14:55:26 -07002163
Clark Boylanb640e052014-04-03 16:41:46 -07002164 def release(self, job):
2165 if isinstance(job, FakeBuild):
2166 job.release()
2167 else:
2168 job.waiting = False
2169 self.log.debug("Queued job %s released" % job.unique)
2170 self.gearman_server.wakeConnections()
2171
2172 def getParameter(self, job, name):
2173 if isinstance(job, FakeBuild):
2174 return job.parameters[name]
2175 else:
2176 parameters = json.loads(job.arguments)
2177 return parameters[name]
2178
Clark Boylanb640e052014-04-03 16:41:46 -07002179 def haveAllBuildsReported(self):
2180 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002181 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002182 return False
2183 # Find out if every build that the worker has completed has been
2184 # reported back to Zuul. If it hasn't then that means a Gearman
2185 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002186 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002187 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002188 if not zbuild:
2189 # It has already been reported
2190 continue
2191 # It hasn't been reported yet.
2192 return False
2193 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002194 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002195 if connection.state == 'GRAB_WAIT':
2196 return False
2197 return True
2198
2199 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002200 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002201 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002202 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002203 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002204 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002205 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002206 for j in conn.related_jobs.values():
2207 if j.unique == build.uuid:
2208 client_job = j
2209 break
2210 if not client_job:
2211 self.log.debug("%s is not known to the gearman client" %
2212 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002213 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002214 if not client_job.handle:
2215 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002216 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002217 server_job = self.gearman_server.jobs.get(client_job.handle)
2218 if not server_job:
2219 self.log.debug("%s is not known to the gearman server" %
2220 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002221 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002222 if not hasattr(server_job, 'waiting'):
2223 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002224 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002225 if server_job.waiting:
2226 continue
James E. Blair17302972016-08-10 16:11:42 -07002227 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002228 self.log.debug("%s has not reported start" % build)
2229 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002230 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002231 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002232 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002233 if worker_build:
2234 if worker_build.isWaiting():
2235 continue
2236 else:
2237 self.log.debug("%s is running" % worker_build)
2238 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002239 else:
James E. Blair962220f2016-08-03 11:22:38 -07002240 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002241 return False
James E. Blaira002b032017-04-18 10:35:48 -07002242 for (build_uuid, job_worker) in \
2243 self.executor_server.job_workers.items():
2244 if build_uuid not in seen_builds:
2245 self.log.debug("%s is not finalized" % build_uuid)
2246 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002247 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002248
James E. Blairdce6cea2016-12-20 16:45:32 -08002249 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002250 if self.fake_nodepool.paused:
2251 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002252 if self.sched.nodepool.requests:
2253 return False
2254 return True
2255
Jan Hruban6b71aff2015-10-22 16:58:08 +02002256 def eventQueuesEmpty(self):
2257 for queue in self.event_queues:
2258 yield queue.empty()
2259
2260 def eventQueuesJoin(self):
2261 for queue in self.event_queues:
2262 queue.join()
2263
Clark Boylanb640e052014-04-03 16:41:46 -07002264 def waitUntilSettled(self):
2265 self.log.debug("Waiting until settled...")
2266 start = time.time()
2267 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002268 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002269 self.log.error("Timeout waiting for Zuul to settle")
2270 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002271 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002272 self.log.error(" %s: %s" % (queue, queue.empty()))
2273 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002274 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002275 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002276 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002277 self.log.error("All requests completed: %s" %
2278 (self.areAllNodeRequestsComplete(),))
2279 self.log.error("Merge client jobs: %s" %
2280 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002281 raise Exception("Timeout waiting for Zuul to settle")
2282 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002283
Paul Belanger174a8272017-03-14 13:20:10 -04002284 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002285 # have all build states propogated to zuul?
2286 if self.haveAllBuildsReported():
2287 # Join ensures that the queue is empty _and_ events have been
2288 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002289 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002290 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002291 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002292 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002293 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002294 self.areAllNodeRequestsComplete() and
2295 all(self.eventQueuesEmpty())):
2296 # The queue empty check is placed at the end to
2297 # ensure that if a component adds an event between
2298 # when locked the run handler and checked that the
2299 # components were stable, we don't erroneously
2300 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002301 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002302 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002303 self.log.debug("...settled.")
2304 return
2305 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002306 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002307 self.sched.wake_event.wait(0.1)
2308
2309 def countJobResults(self, jobs, result):
2310 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002311 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002312
James E. Blair96c6bf82016-01-15 16:20:40 -08002313 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002314 for job in self.history:
2315 if (job.name == name and
2316 (project is None or
2317 job.parameters['ZUUL_PROJECT'] == project)):
2318 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002319 raise Exception("Unable to find job %s in history" % name)
2320
2321 def assertEmptyQueues(self):
2322 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002323 for tenant in self.sched.abide.tenants.values():
2324 for pipeline in tenant.layout.pipelines.values():
2325 for queue in pipeline.queues:
2326 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002327 print('pipeline %s queue %s contents %s' % (
2328 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002329 self.assertEqual(len(queue.queue), 0,
2330 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002331
2332 def assertReportedStat(self, key, value=None, kind=None):
2333 start = time.time()
2334 while time.time() < (start + 5):
2335 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002336 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002337 if key == k:
2338 if value is None and kind is None:
2339 return
2340 elif value:
2341 if value == v:
2342 return
2343 elif kind:
2344 if v.endswith('|' + kind):
2345 return
2346 time.sleep(0.1)
2347
Clark Boylanb640e052014-04-03 16:41:46 -07002348 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002349
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002350 def assertBuilds(self, builds):
2351 """Assert that the running builds are as described.
2352
2353 The list of running builds is examined and must match exactly
2354 the list of builds described by the input.
2355
2356 :arg list builds: A list of dictionaries. Each item in the
2357 list must match the corresponding build in the build
2358 history, and each element of the dictionary must match the
2359 corresponding attribute of the build.
2360
2361 """
James E. Blair3158e282016-08-19 09:34:11 -07002362 try:
2363 self.assertEqual(len(self.builds), len(builds))
2364 for i, d in enumerate(builds):
2365 for k, v in d.items():
2366 self.assertEqual(
2367 getattr(self.builds[i], k), v,
2368 "Element %i in builds does not match" % (i,))
2369 except Exception:
2370 for build in self.builds:
2371 self.log.error("Running build: %s" % build)
2372 else:
2373 self.log.error("No running builds")
2374 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002375
James E. Blairb536ecc2016-08-31 10:11:42 -07002376 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002377 """Assert that the completed builds are as described.
2378
2379 The list of completed builds is examined and must match
2380 exactly the list of builds described by the input.
2381
2382 :arg list history: A list of dictionaries. Each item in the
2383 list must match the corresponding build in the build
2384 history, and each element of the dictionary must match the
2385 corresponding attribute of the build.
2386
James E. Blairb536ecc2016-08-31 10:11:42 -07002387 :arg bool ordered: If true, the history must match the order
2388 supplied, if false, the builds are permitted to have
2389 arrived in any order.
2390
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002391 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002392 def matches(history_item, item):
2393 for k, v in item.items():
2394 if getattr(history_item, k) != v:
2395 return False
2396 return True
James E. Blair3158e282016-08-19 09:34:11 -07002397 try:
2398 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002399 if ordered:
2400 for i, d in enumerate(history):
2401 if not matches(self.history[i], d):
2402 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002403 "Element %i in history does not match %s" %
2404 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002405 else:
2406 unseen = self.history[:]
2407 for i, d in enumerate(history):
2408 found = False
2409 for unseen_item in unseen:
2410 if matches(unseen_item, d):
2411 found = True
2412 unseen.remove(unseen_item)
2413 break
2414 if not found:
2415 raise Exception("No match found for element %i "
2416 "in history" % (i,))
2417 if unseen:
2418 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002419 except Exception:
2420 for build in self.history:
2421 self.log.error("Completed build: %s" % build)
2422 else:
2423 self.log.error("No completed builds")
2424 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002425
James E. Blair6ac368c2016-12-22 18:07:20 -08002426 def printHistory(self):
2427 """Log the build history.
2428
2429 This can be useful during tests to summarize what jobs have
2430 completed.
2431
2432 """
2433 self.log.debug("Build history:")
2434 for build in self.history:
2435 self.log.debug(build)
2436
James E. Blair59fdbac2015-12-07 17:08:06 -08002437 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002438 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2439
James E. Blair9ea70072017-04-19 16:05:30 -07002440 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002441 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002442 if not os.path.exists(root):
2443 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002444 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2445 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002446- tenant:
2447 name: openstack
2448 source:
2449 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002450 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002451 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002452 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002453 - org/project
2454 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002455 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002456 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002457 self.config.set('zuul', 'tenant_config',
2458 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002459 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002460
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002461 def addCommitToRepo(self, project, message, files,
2462 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002463 path = os.path.join(self.upstream_root, project)
2464 repo = git.Repo(path)
2465 repo.head.reference = branch
2466 zuul.merger.merger.reset_repo_to_head(repo)
2467 for fn, content in files.items():
2468 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002469 try:
2470 os.makedirs(os.path.dirname(fn))
2471 except OSError:
2472 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002473 with open(fn, 'w') as f:
2474 f.write(content)
2475 repo.index.add([fn])
2476 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002477 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002478 repo.heads[branch].commit = commit
2479 repo.head.reference = branch
2480 repo.git.clean('-x', '-f', '-d')
2481 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002482 if tag:
2483 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002484 return before
2485
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002486 def commitConfigUpdate(self, project_name, source_name):
2487 """Commit an update to zuul.yaml
2488
2489 This overwrites the zuul.yaml in the specificed project with
2490 the contents specified.
2491
2492 :arg str project_name: The name of the project containing
2493 zuul.yaml (e.g., common-config)
2494
2495 :arg str source_name: The path to the file (underneath the
2496 test fixture directory) whose contents should be used to
2497 replace zuul.yaml.
2498 """
2499
2500 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002501 files = {}
2502 with open(source_path, 'r') as f:
2503 data = f.read()
2504 layout = yaml.safe_load(data)
2505 files['zuul.yaml'] = data
2506 for item in layout:
2507 if 'job' in item:
2508 jobname = item['job']['name']
2509 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002510 before = self.addCommitToRepo(
2511 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002512 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002513 return before
2514
James E. Blair7fc8daa2016-08-08 15:37:15 -07002515 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002516
James E. Blair7fc8daa2016-08-08 15:37:15 -07002517 """Inject a Fake (Gerrit) event.
2518
2519 This method accepts a JSON-encoded event and simulates Zuul
2520 having received it from Gerrit. It could (and should)
2521 eventually apply to any connection type, but is currently only
2522 used with Gerrit connections. The name of the connection is
2523 used to look up the corresponding server, and the event is
2524 simulated as having been received by all Zuul connections
2525 attached to that server. So if two Gerrit connections in Zuul
2526 are connected to the same Gerrit server, and you invoke this
2527 method specifying the name of one of them, the event will be
2528 received by both.
2529
2530 .. note::
2531
2532 "self.fake_gerrit.addEvent" calls should be migrated to
2533 this method.
2534
2535 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002536 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002537 :arg str event: The JSON-encoded event.
2538
2539 """
2540 specified_conn = self.connections.connections[connection]
2541 for conn in self.connections.connections.values():
2542 if (isinstance(conn, specified_conn.__class__) and
2543 specified_conn.server == conn.server):
2544 conn.addEvent(event)
2545
James E. Blair3f876d52016-07-22 13:07:14 -07002546
2547class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002548 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002549 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002550
Joshua Heskethd78b4482015-09-14 16:56:34 -06002551
2552class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002553 def setup_config(self):
2554 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002555 for section_name in self.config.sections():
2556 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2557 section_name, re.I)
2558 if not con_match:
2559 continue
2560
2561 if self.config.get(section_name, 'driver') == 'sql':
2562 f = MySQLSchemaFixture()
2563 self.useFixture(f)
2564 if (self.config.get(section_name, 'dburi') ==
2565 '$MYSQL_FIXTURE_DBURI$'):
2566 self.config.set(section_name, 'dburi', f.dburi)