blob: 8a2928b4be0a9e4c93c5031246b709660c7628dd [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
James E. Blair8b5408c2016-08-08 15:37:46 -0700499 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500 if 'label' in action:
501 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000505
Clark Boylanb640e052014-04-03 16:41:46 -0700506 if 'submit' in action:
507 change.setMerged()
508 if message:
509 change.setReported()
510
511 def query(self, number):
512 change = self.changes.get(int(number))
513 if change:
514 return change.query()
515 return {}
516
James E. Blairc494d542014-08-06 09:23:52 -0700517 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700518 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700519 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800520 if query.startswith('change:'):
521 # Query a specific changeid
522 changeid = query[len('change:'):]
523 l = [change.query() for change in self.changes.values()
524 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700525 elif query.startswith('message:'):
526 # Query the content of a commit message
527 msg = query[len('message:'):].strip()
528 l = [change.query() for change in self.changes.values()
529 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800530 else:
531 # Query all open changes
532 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700533 return l
James E. Blairc494d542014-08-06 09:23:52 -0700534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 pass
537
Joshua Hesketh352264b2015-08-11 23:42:08 +1000538 def getGitUrl(self, project):
539 return os.path.join(self.upstream_root, project.name)
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541
Gregory Haynes4fc12542015-04-22 20:38:06 -0700542class GithubChangeReference(git.Reference):
543 _common_path_default = "refs/pull"
544 _points_to_commits_only = True
545
546
547class FakeGithubPullRequest(object):
548
549 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100550 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551 """Creates a new PR with several commits.
552 Sends an event about opened PR."""
553 self.github = github
554 self.source = github
555 self.number = number
556 self.project = project
557 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100558 self.subject = subject
559 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100561 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700562 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100563 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100564 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700565 self.updated_at = None
566 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100567 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100568 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100577 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700578
Jan Hruban570d01c2016-03-10 21:51:32 +0100579 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100583 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584
585 def getPullRequestOpenedEvent(self):
586 return self._getPullRequestEvent('opened')
587
588 def getPullRequestSynchronizeEvent(self):
589 return self._getPullRequestEvent('synchronize')
590
591 def getPullRequestReopenedEvent(self):
592 return self._getPullRequestEvent('reopened')
593
594 def getPullRequestClosedEvent(self):
595 return self._getPullRequestEvent('closed')
596
597 def addComment(self, message):
598 self.comments.append(message)
599 self._updateTimeStamp()
600
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200601 def getCommentAddedEvent(self, text):
602 name = 'issue_comment'
603 data = {
604 'action': 'created',
605 'issue': {
606 'number': self.number
607 },
608 'comment': {
609 'body': text
610 },
611 'repository': {
612 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100613 },
614 'sender': {
615 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200616 }
617 }
618 return (name, data)
619
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800620 def getReviewAddedEvent(self, review):
621 name = 'pull_request_review'
622 data = {
623 'action': 'submitted',
624 'pull_request': {
625 'number': self.number,
626 'title': self.subject,
627 'updated_at': self.updated_at,
628 'base': {
629 'ref': self.branch,
630 'repo': {
631 'full_name': self.project
632 }
633 },
634 'head': {
635 'sha': self.head_sha
636 }
637 },
638 'review': {
639 'state': review
640 },
641 'repository': {
642 'full_name': self.project
643 },
644 'sender': {
645 'login': 'ghuser'
646 }
647 }
648 return (name, data)
649
Jan Hruban16ad31f2015-11-07 14:39:07 +0100650 def addLabel(self, name):
651 if name not in self.labels:
652 self.labels.append(name)
653 self._updateTimeStamp()
654 return self._getLabelEvent(name)
655
656 def removeLabel(self, name):
657 if name in self.labels:
658 self.labels.remove(name)
659 self._updateTimeStamp()
660 return self._getUnlabelEvent(name)
661
662 def _getLabelEvent(self, label):
663 name = 'pull_request'
664 data = {
665 'action': 'labeled',
666 'pull_request': {
667 'number': self.number,
668 'updated_at': self.updated_at,
669 'base': {
670 'ref': self.branch,
671 'repo': {
672 'full_name': self.project
673 }
674 },
675 'head': {
676 'sha': self.head_sha
677 }
678 },
679 'label': {
680 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100681 },
682 'sender': {
683 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100684 }
685 }
686 return (name, data)
687
688 def _getUnlabelEvent(self, label):
689 name = 'pull_request'
690 data = {
691 'action': 'unlabeled',
692 'pull_request': {
693 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100694 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 'updated_at': self.updated_at,
696 'base': {
697 'ref': self.branch,
698 'repo': {
699 'full_name': self.project
700 }
701 },
702 'head': {
703 'sha': self.head_sha
704 }
705 },
706 'label': {
707 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100708 },
709 'sender': {
710 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100711 }
712 }
713 return (name, data)
714
Gregory Haynes4fc12542015-04-22 20:38:06 -0700715 def _getRepo(self):
716 repo_path = os.path.join(self.upstream_root, self.project)
717 return git.Repo(repo_path)
718
719 def _createPRRef(self):
720 repo = self._getRepo()
721 GithubChangeReference.create(
722 repo, self._getPRReference(), 'refs/tags/init')
723
Jan Hruban570d01c2016-03-10 21:51:32 +0100724 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 repo = self._getRepo()
726 ref = repo.references[self._getPRReference()]
727 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100728 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100730 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 repo.head.reference = ref
732 zuul.merger.merger.reset_repo_to_head(repo)
733 repo.git.clean('-x', '-f', '-d')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 if files:
736 fn = files[0]
737 self.files = files
738 else:
739 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
740 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100741 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 fn = os.path.join(repo.working_dir, fn)
743 f = open(fn, 'w')
744 with open(fn, 'w') as f:
745 f.write("test %s %s\n" %
746 (self.branch, self.number))
747 repo.index.add([fn])
748
749 self.head_sha = repo.index.commit(msg).hexsha
750 repo.head.reference = 'master'
751 zuul.merger.merger.reset_repo_to_head(repo)
752 repo.git.clean('-x', '-f', '-d')
753 repo.heads['master'].checkout()
754
755 def _updateTimeStamp(self):
756 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
757
758 def getPRHeadSha(self):
759 repo = self._getRepo()
760 return repo.references[self._getPRReference()].commit.hexsha
761
Jan Hrubane252a732017-01-03 15:03:09 +0100762 def setStatus(self, state, url, description, context):
763 self.statuses[context] = {
764 'state': state,
765 'url': url,
766 'description': description
767 }
768
769 def _clearStatuses(self):
770 self.statuses = {}
771
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 def _getPRReference(self):
773 return '%s/head' % self.number
774
775 def _getPullRequestEvent(self, action):
776 name = 'pull_request'
777 data = {
778 'action': action,
779 'number': self.number,
780 'pull_request': {
781 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100782 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700783 'updated_at': self.updated_at,
784 'base': {
785 'ref': self.branch,
786 'repo': {
787 'full_name': self.project
788 }
789 },
790 'head': {
791 'sha': self.head_sha
792 }
Jan Hruban3b415922016-02-03 13:10:22 +0100793 },
794 'sender': {
795 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700796 }
797 }
798 return (name, data)
799
800
801class FakeGithubConnection(githubconnection.GithubConnection):
802 log = logging.getLogger("zuul.test.FakeGithubConnection")
803
804 def __init__(self, driver, connection_name, connection_config,
805 upstream_root=None):
806 super(FakeGithubConnection, self).__init__(driver, connection_name,
807 connection_config)
808 self.connection_name = connection_name
809 self.pr_number = 0
810 self.pull_requests = []
811 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100812 self.merge_failure = False
813 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814
Jan Hruban570d01c2016-03-10 21:51:32 +0100815 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 self.pr_number += 1
817 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100818 self, self.pr_number, project, branch, subject, self.upstream_root,
819 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700820 self.pull_requests.append(pull_request)
821 return pull_request
822
Wayne1a78c612015-06-11 17:14:13 -0700823 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
824 if not old_rev:
825 old_rev = '00000000000000000000000000000000'
826 if not new_rev:
827 new_rev = random_sha1()
828 name = 'push'
829 data = {
830 'ref': ref,
831 'before': old_rev,
832 'after': new_rev,
833 'repository': {
834 'full_name': project
835 }
836 }
837 return (name, data)
838
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 def emitEvent(self, event):
840 """Emulates sending the GitHub webhook event to the connection."""
841 port = self.webapp.server.socket.getsockname()[1]
842 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700843 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700844 headers = {'X-Github-Event': name}
845 req = urllib.request.Request(
846 'http://localhost:%s/connection/%s/payload'
847 % (port, self.connection_name),
848 data=payload, headers=headers)
849 urllib.request.urlopen(req)
850
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200851 def getPull(self, project, number):
852 pr = self.pull_requests[number - 1]
853 data = {
854 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100855 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200856 'updated_at': pr.updated_at,
857 'base': {
858 'repo': {
859 'full_name': pr.project
860 },
861 'ref': pr.branch,
862 },
Jan Hruban37615e52015-11-19 14:30:49 +0100863 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200864 'head': {
865 'sha': pr.head_sha
866 }
867 }
868 return data
869
Jan Hruban570d01c2016-03-10 21:51:32 +0100870 def getPullFileNames(self, project, number):
871 pr = self.pull_requests[number - 1]
872 return pr.files
873
Jan Hruban3b415922016-02-03 13:10:22 +0100874 def getUser(self, login):
875 data = {
876 'username': login,
877 'name': 'Github User',
878 'email': 'github.user@example.com'
879 }
880 return data
881
Gregory Haynes4fc12542015-04-22 20:38:06 -0700882 def getGitUrl(self, project):
883 return os.path.join(self.upstream_root, str(project))
884
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200885 def real_getGitUrl(self, project):
886 return super(FakeGithubConnection, self).getGitUrl(project)
887
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888 def getProjectBranches(self, project):
889 """Masks getProjectBranches since we don't have a real github"""
890
891 # just returns master for now
892 return ['master']
893
Jan Hrubane252a732017-01-03 15:03:09 +0100894 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700895 pull_request = self.pull_requests[pr_number - 1]
896 pull_request.addComment(message)
897
Jan Hruban3b415922016-02-03 13:10:22 +0100898 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100899 pull_request = self.pull_requests[pr_number - 1]
900 if self.merge_failure:
901 raise Exception('Pull request was not merged')
902 if self.merge_not_allowed_count > 0:
903 self.merge_not_allowed_count -= 1
904 raise MergeFailure('Merge was not successful due to mergeability'
905 ' conflict')
906 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100907 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100908
Jan Hrubane252a732017-01-03 15:03:09 +0100909 def setCommitStatus(self, project, sha, state,
910 url='', description='', context=''):
911 owner, proj = project.split('/')
912 for pr in self.pull_requests:
913 pr_owner, pr_project = pr.project.split('/')
914 if (pr_owner == owner and pr_project == proj and
915 pr.head_sha == sha):
916 pr.setStatus(state, url, description, context)
917
Jan Hruban16ad31f2015-11-07 14:39:07 +0100918 def labelPull(self, project, pr_number, label):
919 pull_request = self.pull_requests[pr_number - 1]
920 pull_request.addLabel(label)
921
922 def unlabelPull(self, project, pr_number, label):
923 pull_request = self.pull_requests[pr_number - 1]
924 pull_request.removeLabel(label)
925
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926
Clark Boylanb640e052014-04-03 16:41:46 -0700927class BuildHistory(object):
928 def __init__(self, **kw):
929 self.__dict__.update(kw)
930
931 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700932 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
933 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700934
935
936class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200937 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700938 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700939 self.url = url
940
941 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700942 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700943 path = res.path
944 project = '/'.join(path.split('/')[2:-2])
945 ret = '001e# service=git-upload-pack\n'
946 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
947 'multi_ack thin-pack side-band side-band-64k ofs-delta '
948 'shallow no-progress include-tag multi_ack_detailed no-done\n')
949 path = os.path.join(self.upstream_root, project)
950 repo = git.Repo(path)
951 for ref in repo.refs:
952 r = ref.object.hexsha + ' ' + ref.path + '\n'
953 ret += '%04x%s' % (len(r) + 4, r)
954 ret += '0000'
955 return ret
956
957
Clark Boylanb640e052014-04-03 16:41:46 -0700958class FakeStatsd(threading.Thread):
959 def __init__(self):
960 threading.Thread.__init__(self)
961 self.daemon = True
962 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
963 self.sock.bind(('', 0))
964 self.port = self.sock.getsockname()[1]
965 self.wake_read, self.wake_write = os.pipe()
966 self.stats = []
967
968 def run(self):
969 while True:
970 poll = select.poll()
971 poll.register(self.sock, select.POLLIN)
972 poll.register(self.wake_read, select.POLLIN)
973 ret = poll.poll()
974 for (fd, event) in ret:
975 if fd == self.sock.fileno():
976 data = self.sock.recvfrom(1024)
977 if not data:
978 return
979 self.stats.append(data[0])
980 if fd == self.wake_read:
981 return
982
983 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -0700984 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -0700985
986
James E. Blaire1767bc2016-08-02 10:00:27 -0700987class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700988 log = logging.getLogger("zuul.test")
989
Paul Belanger174a8272017-03-14 13:20:10 -0400990 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700991 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400992 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700994 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700995 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700996 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700997 # TODOv3(jeblair): self.node is really "the image of the node
998 # assigned". We should rename it (self.node_image?) if we
999 # keep using it like this, or we may end up exposing more of
1000 # the complexity around multi-node jobs here
1001 # (self.nodes[0].image?)
1002 self.node = None
1003 if len(self.parameters.get('nodes')) == 1:
1004 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001005 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001006 self.pipeline = self.parameters['ZUUL_PIPELINE']
1007 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001008 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001009 self.wait_condition = threading.Condition()
1010 self.waiting = False
1011 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001012 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001013 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001014 self.changes = None
1015 if 'ZUUL_CHANGE_IDS' in self.parameters:
1016 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001017
James E. Blair3158e282016-08-19 09:34:11 -07001018 def __repr__(self):
1019 waiting = ''
1020 if self.waiting:
1021 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001022 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1023 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001024
Clark Boylanb640e052014-04-03 16:41:46 -07001025 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001026 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001027 self.wait_condition.acquire()
1028 self.wait_condition.notify()
1029 self.waiting = False
1030 self.log.debug("Build %s released" % self.unique)
1031 self.wait_condition.release()
1032
1033 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001034 """Return whether this build is being held.
1035
1036 :returns: Whether the build is being held.
1037 :rtype: bool
1038 """
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040 self.wait_condition.acquire()
1041 if self.waiting:
1042 ret = True
1043 else:
1044 ret = False
1045 self.wait_condition.release()
1046 return ret
1047
1048 def _wait(self):
1049 self.wait_condition.acquire()
1050 self.waiting = True
1051 self.log.debug("Build %s waiting" % self.unique)
1052 self.wait_condition.wait()
1053 self.wait_condition.release()
1054
1055 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001056 self.log.debug('Running build %s' % self.unique)
1057
Paul Belanger174a8272017-03-14 13:20:10 -04001058 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001059 self.log.debug('Holding build %s' % self.unique)
1060 self._wait()
1061 self.log.debug("Build %s continuing" % self.unique)
1062
James E. Blair412fba82017-01-26 15:00:50 -08001063 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001064 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001065 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001066 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001067 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001068 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001069 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001070
James E. Blaire1767bc2016-08-02 10:00:27 -07001071 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001072
James E. Blaira5dba232016-08-08 15:53:24 -07001073 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001074 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001075 for change in changes:
1076 if self.hasChanges(change):
1077 return True
1078 return False
1079
James E. Blaire7b99a02016-08-05 14:27:34 -07001080 def hasChanges(self, *changes):
1081 """Return whether this build has certain changes in its git repos.
1082
1083 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001084 are expected to be present (in order) in the git repository of
1085 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001086
1087 :returns: Whether the build has the indicated changes.
1088 :rtype: bool
1089
1090 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001091 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001092 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001093 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001094 try:
1095 repo = git.Repo(path)
1096 except NoSuchPathError as e:
1097 self.log.debug('%s' % e)
1098 return False
1099 ref = self.parameters['ZUUL_REF']
1100 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1101 commit_message = '%s-1' % change.subject
1102 self.log.debug("Checking if build %s has changes; commit_message "
1103 "%s; repo_messages %s" % (self, commit_message,
1104 repo_messages))
1105 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001106 self.log.debug(" messages do not match")
1107 return False
1108 self.log.debug(" OK")
1109 return True
1110
Clark Boylanb640e052014-04-03 16:41:46 -07001111
Paul Belanger174a8272017-03-14 13:20:10 -04001112class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1113 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001114
Paul Belanger174a8272017-03-14 13:20:10 -04001115 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001116 they will report that they have started but then pause until
1117 released before reporting completion. This attribute may be
1118 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001119 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 be explicitly released.
1121
1122 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001123 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001124 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001125 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001126 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.hold_jobs_in_build = False
1128 self.lock = threading.Lock()
1129 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001130 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001131 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001132 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001133
James E. Blaira5dba232016-08-08 15:53:24 -07001134 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001135 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001136
1137 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001138 :arg Change change: The :py:class:`~tests.base.FakeChange`
1139 instance which should cause the job to fail. This job
1140 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001141
1142 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001143 l = self.fail_tests.get(name, [])
1144 l.append(change)
1145 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001146
James E. Blair962220f2016-08-03 11:22:38 -07001147 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001148 """Release a held build.
1149
1150 :arg str regex: A regular expression which, if supplied, will
1151 cause only builds with matching names to be released. If
1152 not supplied, all builds will be released.
1153
1154 """
James E. Blair962220f2016-08-03 11:22:38 -07001155 builds = self.running_builds[:]
1156 self.log.debug("Releasing build %s (%s)" % (regex,
1157 len(self.running_builds)))
1158 for build in builds:
1159 if not regex or re.match(regex, build.name):
1160 self.log.debug("Releasing build %s" %
1161 (build.parameters['ZUUL_UUID']))
1162 build.release()
1163 else:
1164 self.log.debug("Not releasing build %s" %
1165 (build.parameters['ZUUL_UUID']))
1166 self.log.debug("Done releasing builds %s (%s)" %
1167 (regex, len(self.running_builds)))
1168
Paul Belanger174a8272017-03-14 13:20:10 -04001169 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001170 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001171 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001172 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001173 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001174 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001175 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001176 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001177 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1178 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001179
1180 def stopJob(self, job):
1181 self.log.debug("handle stop")
1182 parameters = json.loads(job.arguments)
1183 uuid = parameters['uuid']
1184 for build in self.running_builds:
1185 if build.unique == uuid:
1186 build.aborted = True
1187 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001188 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001189
James E. Blaira002b032017-04-18 10:35:48 -07001190 def stop(self):
1191 for build in self.running_builds:
1192 build.release()
1193 super(RecordingExecutorServer, self).stop()
1194
Joshua Hesketh50c21782016-10-13 21:34:14 +11001195
Paul Belanger174a8272017-03-14 13:20:10 -04001196class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001197 def doMergeChanges(self, items):
1198 # Get a merger in order to update the repos involved in this job.
1199 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1200 if not commit: # merge conflict
1201 self.recordResult('MERGER_FAILURE')
1202 return commit
1203
1204 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001205 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001206 self.executor_server.lock.acquire()
1207 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001208 BuildHistory(name=build.name, result=result, changes=build.changes,
1209 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001210 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001211 pipeline=build.parameters['ZUUL_PIPELINE'])
1212 )
Paul Belanger174a8272017-03-14 13:20:10 -04001213 self.executor_server.running_builds.remove(build)
1214 del self.executor_server.job_builds[self.job.unique]
1215 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001216
1217 def runPlaybooks(self, args):
1218 build = self.executor_server.job_builds[self.job.unique]
1219 build.jobdir = self.jobdir
1220
1221 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1222 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001223 return result
1224
Monty Taylore6562aa2017-02-20 07:37:39 -05001225 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001226 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001229 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001230 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001231 else:
1232 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001233 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001234
James E. Blairad8dca02017-02-21 11:48:32 -05001235 def getHostList(self, args):
1236 self.log.debug("hostlist")
1237 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001238 for host in hosts:
1239 host['host_vars']['ansible_connection'] = 'local'
1240
1241 hosts.append(dict(
1242 name='localhost',
1243 host_vars=dict(ansible_connection='local'),
1244 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001245 return hosts
1246
James E. Blairf5dbd002015-12-23 15:26:17 -08001247
Clark Boylanb640e052014-04-03 16:41:46 -07001248class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001249 """A Gearman server for use in tests.
1250
1251 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1252 added to the queue but will not be distributed to workers
1253 until released. This attribute may be changed at any time and
1254 will take effect for subsequently enqueued jobs, but
1255 previously held jobs will still need to be explicitly
1256 released.
1257
1258 """
1259
Clark Boylanb640e052014-04-03 16:41:46 -07001260 def __init__(self):
1261 self.hold_jobs_in_queue = False
1262 super(FakeGearmanServer, self).__init__(0)
1263
1264 def getJobForConnection(self, connection, peek=False):
1265 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1266 for job in queue:
1267 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001268 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001269 job.waiting = self.hold_jobs_in_queue
1270 else:
1271 job.waiting = False
1272 if job.waiting:
1273 continue
1274 if job.name in connection.functions:
1275 if not peek:
1276 queue.remove(job)
1277 connection.related_jobs[job.handle] = job
1278 job.worker_connection = connection
1279 job.running = True
1280 return job
1281 return None
1282
1283 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001284 """Release a held job.
1285
1286 :arg str regex: A regular expression which, if supplied, will
1287 cause only jobs with matching names to be released. If
1288 not supplied, all jobs will be released.
1289 """
Clark Boylanb640e052014-04-03 16:41:46 -07001290 released = False
1291 qlen = (len(self.high_queue) + len(self.normal_queue) +
1292 len(self.low_queue))
1293 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1294 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001295 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001296 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001297 parameters = json.loads(job.arguments)
1298 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.log.debug("releasing queued job %s" %
1300 job.unique)
1301 job.waiting = False
1302 released = True
1303 else:
1304 self.log.debug("not releasing queued job %s" %
1305 job.unique)
1306 if released:
1307 self.wakeConnections()
1308 qlen = (len(self.high_queue) + len(self.normal_queue) +
1309 len(self.low_queue))
1310 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1311
1312
1313class FakeSMTP(object):
1314 log = logging.getLogger('zuul.FakeSMTP')
1315
1316 def __init__(self, messages, server, port):
1317 self.server = server
1318 self.port = port
1319 self.messages = messages
1320
1321 def sendmail(self, from_email, to_email, msg):
1322 self.log.info("Sending email from %s, to %s, with msg %s" % (
1323 from_email, to_email, msg))
1324
1325 headers = msg.split('\n\n', 1)[0]
1326 body = msg.split('\n\n', 1)[1]
1327
1328 self.messages.append(dict(
1329 from_email=from_email,
1330 to_email=to_email,
1331 msg=msg,
1332 headers=headers,
1333 body=body,
1334 ))
1335
1336 return True
1337
1338 def quit(self):
1339 return True
1340
1341
James E. Blairdce6cea2016-12-20 16:45:32 -08001342class FakeNodepool(object):
1343 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001344 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001345
1346 log = logging.getLogger("zuul.test.FakeNodepool")
1347
1348 def __init__(self, host, port, chroot):
1349 self.client = kazoo.client.KazooClient(
1350 hosts='%s:%s%s' % (host, port, chroot))
1351 self.client.start()
1352 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001353 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001354 self.thread = threading.Thread(target=self.run)
1355 self.thread.daemon = True
1356 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001357 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001358
1359 def stop(self):
1360 self._running = False
1361 self.thread.join()
1362 self.client.stop()
1363 self.client.close()
1364
1365 def run(self):
1366 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001367 try:
1368 self._run()
1369 except Exception:
1370 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001371 time.sleep(0.1)
1372
1373 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001374 if self.paused:
1375 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001376 for req in self.getNodeRequests():
1377 self.fulfillRequest(req)
1378
1379 def getNodeRequests(self):
1380 try:
1381 reqids = self.client.get_children(self.REQUEST_ROOT)
1382 except kazoo.exceptions.NoNodeError:
1383 return []
1384 reqs = []
1385 for oid in sorted(reqids):
1386 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001387 try:
1388 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001389 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001390 data['_oid'] = oid
1391 reqs.append(data)
1392 except kazoo.exceptions.NoNodeError:
1393 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001394 return reqs
1395
James E. Blaire18d4602017-01-05 11:17:28 -08001396 def getNodes(self):
1397 try:
1398 nodeids = self.client.get_children(self.NODE_ROOT)
1399 except kazoo.exceptions.NoNodeError:
1400 return []
1401 nodes = []
1402 for oid in sorted(nodeids):
1403 path = self.NODE_ROOT + '/' + oid
1404 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001405 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001406 data['_oid'] = oid
1407 try:
1408 lockfiles = self.client.get_children(path + '/lock')
1409 except kazoo.exceptions.NoNodeError:
1410 lockfiles = []
1411 if lockfiles:
1412 data['_lock'] = True
1413 else:
1414 data['_lock'] = False
1415 nodes.append(data)
1416 return nodes
1417
James E. Blaira38c28e2017-01-04 10:33:20 -08001418 def makeNode(self, request_id, node_type):
1419 now = time.time()
1420 path = '/nodepool/nodes/'
1421 data = dict(type=node_type,
1422 provider='test-provider',
1423 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001424 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001425 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001426 public_ipv4='127.0.0.1',
1427 private_ipv4=None,
1428 public_ipv6=None,
1429 allocated_to=request_id,
1430 state='ready',
1431 state_time=now,
1432 created_time=now,
1433 updated_time=now,
1434 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001435 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001436 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001437 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001438 path = self.client.create(path, data,
1439 makepath=True,
1440 sequence=True)
1441 nodeid = path.split("/")[-1]
1442 return nodeid
1443
James E. Blair6ab79e02017-01-06 10:10:17 -08001444 def addFailRequest(self, request):
1445 self.fail_requests.add(request['_oid'])
1446
James E. Blairdce6cea2016-12-20 16:45:32 -08001447 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001448 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001449 return
1450 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001451 oid = request['_oid']
1452 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001453
James E. Blair6ab79e02017-01-06 10:10:17 -08001454 if oid in self.fail_requests:
1455 request['state'] = 'failed'
1456 else:
1457 request['state'] = 'fulfilled'
1458 nodes = []
1459 for node in request['node_types']:
1460 nodeid = self.makeNode(oid, node)
1461 nodes.append(nodeid)
1462 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001463
James E. Blaira38c28e2017-01-04 10:33:20 -08001464 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001465 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001466 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001467 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001468 try:
1469 self.client.set(path, data)
1470 except kazoo.exceptions.NoNodeError:
1471 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001472
1473
James E. Blair498059b2016-12-20 13:50:13 -08001474class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001475 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001476 super(ChrootedKazooFixture, self).__init__()
1477
1478 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1479 if ':' in zk_host:
1480 host, port = zk_host.split(':')
1481 else:
1482 host = zk_host
1483 port = None
1484
1485 self.zookeeper_host = host
1486
1487 if not port:
1488 self.zookeeper_port = 2181
1489 else:
1490 self.zookeeper_port = int(port)
1491
Clark Boylan621ec9a2017-04-07 17:41:33 -07001492 self.test_id = test_id
1493
James E. Blair498059b2016-12-20 13:50:13 -08001494 def _setUp(self):
1495 # Make sure the test chroot paths do not conflict
1496 random_bits = ''.join(random.choice(string.ascii_lowercase +
1497 string.ascii_uppercase)
1498 for x in range(8))
1499
Clark Boylan621ec9a2017-04-07 17:41:33 -07001500 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001501 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1502
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001503 self.addCleanup(self._cleanup)
1504
James E. Blair498059b2016-12-20 13:50:13 -08001505 # Ensure the chroot path exists and clean up any pre-existing znodes.
1506 _tmp_client = kazoo.client.KazooClient(
1507 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1508 _tmp_client.start()
1509
1510 if _tmp_client.exists(self.zookeeper_chroot):
1511 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1512
1513 _tmp_client.ensure_path(self.zookeeper_chroot)
1514 _tmp_client.stop()
1515 _tmp_client.close()
1516
James E. Blair498059b2016-12-20 13:50:13 -08001517 def _cleanup(self):
1518 '''Remove the chroot path.'''
1519 # Need a non-chroot'ed client to remove the chroot path
1520 _tmp_client = kazoo.client.KazooClient(
1521 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1522 _tmp_client.start()
1523 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1524 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001525 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001526
1527
Joshua Heskethd78b4482015-09-14 16:56:34 -06001528class MySQLSchemaFixture(fixtures.Fixture):
1529 def setUp(self):
1530 super(MySQLSchemaFixture, self).setUp()
1531
1532 random_bits = ''.join(random.choice(string.ascii_lowercase +
1533 string.ascii_uppercase)
1534 for x in range(8))
1535 self.name = '%s_%s' % (random_bits, os.getpid())
1536 self.passwd = uuid.uuid4().hex
1537 db = pymysql.connect(host="localhost",
1538 user="openstack_citest",
1539 passwd="openstack_citest",
1540 db="openstack_citest")
1541 cur = db.cursor()
1542 cur.execute("create database %s" % self.name)
1543 cur.execute(
1544 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1545 (self.name, self.name, self.passwd))
1546 cur.execute("flush privileges")
1547
1548 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1549 self.passwd,
1550 self.name)
1551 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1552 self.addCleanup(self.cleanup)
1553
1554 def cleanup(self):
1555 db = pymysql.connect(host="localhost",
1556 user="openstack_citest",
1557 passwd="openstack_citest",
1558 db="openstack_citest")
1559 cur = db.cursor()
1560 cur.execute("drop database %s" % self.name)
1561 cur.execute("drop user '%s'@'localhost'" % self.name)
1562 cur.execute("flush privileges")
1563
1564
Maru Newby3fe5f852015-01-13 04:22:14 +00001565class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001566 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001567 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001568
James E. Blair1c236df2017-02-01 14:07:24 -08001569 def attachLogs(self, *args):
1570 def reader():
1571 self._log_stream.seek(0)
1572 while True:
1573 x = self._log_stream.read(4096)
1574 if not x:
1575 break
1576 yield x.encode('utf8')
1577 content = testtools.content.content_from_reader(
1578 reader,
1579 testtools.content_type.UTF8_TEXT,
1580 False)
1581 self.addDetail('logging', content)
1582
Clark Boylanb640e052014-04-03 16:41:46 -07001583 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001584 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001585 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1586 try:
1587 test_timeout = int(test_timeout)
1588 except ValueError:
1589 # If timeout value is invalid do not set a timeout.
1590 test_timeout = 0
1591 if test_timeout > 0:
1592 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1593
1594 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1595 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1596 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1597 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1598 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1599 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1600 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1601 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1602 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1603 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001604 self._log_stream = StringIO()
1605 self.addOnException(self.attachLogs)
1606 else:
1607 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001608
James E. Blair73b41772017-05-22 13:22:55 -07001609 # NOTE(jeblair): this is temporary extra debugging to try to
1610 # track down a possible leak.
1611 orig_git_repo_init = git.Repo.__init__
1612
1613 def git_repo_init(myself, *args, **kw):
1614 orig_git_repo_init(myself, *args, **kw)
1615 self.log.debug("Created git repo 0x%x %s" %
1616 (id(myself), repr(myself)))
1617
1618 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1619 git_repo_init))
1620
James E. Blair1c236df2017-02-01 14:07:24 -08001621 handler = logging.StreamHandler(self._log_stream)
1622 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1623 '%(levelname)-8s %(message)s')
1624 handler.setFormatter(formatter)
1625
1626 logger = logging.getLogger()
1627 logger.setLevel(logging.DEBUG)
1628 logger.addHandler(handler)
1629
Clark Boylan3410d532017-04-25 12:35:29 -07001630 # Make sure we don't carry old handlers around in process state
1631 # which slows down test runs
1632 self.addCleanup(logger.removeHandler, handler)
1633 self.addCleanup(handler.close)
1634 self.addCleanup(handler.flush)
1635
James E. Blair1c236df2017-02-01 14:07:24 -08001636 # NOTE(notmorgan): Extract logging overrides for specific
1637 # libraries from the OS_LOG_DEFAULTS env and create loggers
1638 # for each. This is used to limit the output during test runs
1639 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001640 log_defaults_from_env = os.environ.get(
1641 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001642 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001643
James E. Blairdce6cea2016-12-20 16:45:32 -08001644 if log_defaults_from_env:
1645 for default in log_defaults_from_env.split(','):
1646 try:
1647 name, level_str = default.split('=', 1)
1648 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001649 logger = logging.getLogger(name)
1650 logger.setLevel(level)
1651 logger.addHandler(handler)
1652 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001653 except ValueError:
1654 # NOTE(notmorgan): Invalid format of the log default,
1655 # skip and don't try and apply a logger for the
1656 # specified module
1657 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001658
Maru Newby3fe5f852015-01-13 04:22:14 +00001659
1660class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001661 """A test case with a functioning Zuul.
1662
1663 The following class variables are used during test setup and can
1664 be overidden by subclasses but are effectively read-only once a
1665 test method starts running:
1666
1667 :cvar str config_file: This points to the main zuul config file
1668 within the fixtures directory. Subclasses may override this
1669 to obtain a different behavior.
1670
1671 :cvar str tenant_config_file: This is the tenant config file
1672 (which specifies from what git repos the configuration should
1673 be loaded). It defaults to the value specified in
1674 `config_file` but can be overidden by subclasses to obtain a
1675 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001676 configuration. See also the :py:func:`simple_layout`
1677 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001678
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001679 :cvar bool create_project_keys: Indicates whether Zuul should
1680 auto-generate keys for each project, or whether the test
1681 infrastructure should insert dummy keys to save time during
1682 startup. Defaults to False.
1683
James E. Blaire7b99a02016-08-05 14:27:34 -07001684 The following are instance variables that are useful within test
1685 methods:
1686
1687 :ivar FakeGerritConnection fake_<connection>:
1688 A :py:class:`~tests.base.FakeGerritConnection` will be
1689 instantiated for each connection present in the config file
1690 and stored here. For instance, `fake_gerrit` will hold the
1691 FakeGerritConnection object for a connection named `gerrit`.
1692
1693 :ivar FakeGearmanServer gearman_server: An instance of
1694 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1695 server that all of the Zuul components in this test use to
1696 communicate with each other.
1697
Paul Belanger174a8272017-03-14 13:20:10 -04001698 :ivar RecordingExecutorServer executor_server: An instance of
1699 :py:class:`~tests.base.RecordingExecutorServer` which is the
1700 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001701
1702 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1703 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001704 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001705 list upon completion.
1706
1707 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1708 objects representing completed builds. They are appended to
1709 the list in the order they complete.
1710
1711 """
1712
James E. Blair83005782015-12-11 14:46:03 -08001713 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001714 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001715 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001716
1717 def _startMerger(self):
1718 self.merge_server = zuul.merger.server.MergeServer(self.config,
1719 self.connections)
1720 self.merge_server.start()
1721
Maru Newby3fe5f852015-01-13 04:22:14 +00001722 def setUp(self):
1723 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001724
1725 self.setupZK()
1726
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001727 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001728 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001729 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1730 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001731 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001732 tmp_root = tempfile.mkdtemp(
1733 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001734 self.test_root = os.path.join(tmp_root, "zuul-test")
1735 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001736 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001737 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001738 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001739
1740 if os.path.exists(self.test_root):
1741 shutil.rmtree(self.test_root)
1742 os.makedirs(self.test_root)
1743 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001744 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001745
1746 # Make per test copy of Configuration.
1747 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001748 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001749 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001750 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001751 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001752 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001753 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001754
Clark Boylanb640e052014-04-03 16:41:46 -07001755 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001756 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1757 # see: https://github.com/jsocol/pystatsd/issues/61
1758 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001759 os.environ['STATSD_PORT'] = str(self.statsd.port)
1760 self.statsd.start()
1761 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001762 reload_module(statsd)
1763 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001764
1765 self.gearman_server = FakeGearmanServer()
1766
1767 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001768 self.log.info("Gearman server on port %s" %
1769 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001770
James E. Blaire511d2f2016-12-08 15:22:26 -08001771 gerritsource.GerritSource.replication_timeout = 1.5
1772 gerritsource.GerritSource.replication_retry_interval = 0.5
1773 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001774
Joshua Hesketh352264b2015-08-11 23:42:08 +10001775 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001776
Jan Hruban7083edd2015-08-21 14:00:54 +02001777 self.webapp = zuul.webapp.WebApp(
1778 self.sched, port=0, listen_address='127.0.0.1')
1779
Jan Hruban6b71aff2015-10-22 16:58:08 +02001780 self.event_queues = [
1781 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001782 self.sched.trigger_event_queue,
1783 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001784 ]
1785
James E. Blairfef78942016-03-11 16:28:56 -08001786 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001787 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001788
Clark Boylanb640e052014-04-03 16:41:46 -07001789 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001790 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001791 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001792 return FakeURLOpener(self.upstream_root, *args, **kw)
1793
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001794 old_urlopen = urllib.request.urlopen
1795 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001796
James E. Blair3f876d52016-07-22 13:07:14 -07001797 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001798
Paul Belanger174a8272017-03-14 13:20:10 -04001799 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001800 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001801 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001802 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001803 _test_root=self.test_root,
1804 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001805 self.executor_server.start()
1806 self.history = self.executor_server.build_history
1807 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001808
Paul Belanger174a8272017-03-14 13:20:10 -04001809 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001810 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001811 self.merge_client = zuul.merger.client.MergeClient(
1812 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001813 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001814 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001815 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001816
James E. Blair0d5a36e2017-02-21 10:53:44 -05001817 self.fake_nodepool = FakeNodepool(
1818 self.zk_chroot_fixture.zookeeper_host,
1819 self.zk_chroot_fixture.zookeeper_port,
1820 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001821
Paul Belanger174a8272017-03-14 13:20:10 -04001822 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001823 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001824 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001825 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001826
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001827 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001828
1829 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001830 self.webapp.start()
1831 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001832 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001833 # Cleanups are run in reverse order
1834 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001835 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001836 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001837
James E. Blairb9c0d772017-03-03 14:34:49 -08001838 self.sched.reconfigure(self.config)
1839 self.sched.resume()
1840
James E. Blairfef78942016-03-11 16:28:56 -08001841 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001842 # Set up gerrit related fakes
1843 # Set a changes database so multiple FakeGerrit's can report back to
1844 # a virtual canonical database given by the configured hostname
1845 self.gerrit_changes_dbs = {}
1846
1847 def getGerritConnection(driver, name, config):
1848 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1849 con = FakeGerritConnection(driver, name, config,
1850 changes_db=db,
1851 upstream_root=self.upstream_root)
1852 self.event_queues.append(con.event_queue)
1853 setattr(self, 'fake_' + name, con)
1854 return con
1855
1856 self.useFixture(fixtures.MonkeyPatch(
1857 'zuul.driver.gerrit.GerritDriver.getConnection',
1858 getGerritConnection))
1859
Gregory Haynes4fc12542015-04-22 20:38:06 -07001860 def getGithubConnection(driver, name, config):
1861 con = FakeGithubConnection(driver, name, config,
1862 upstream_root=self.upstream_root)
1863 setattr(self, 'fake_' + name, con)
1864 return con
1865
1866 self.useFixture(fixtures.MonkeyPatch(
1867 'zuul.driver.github.GithubDriver.getConnection',
1868 getGithubConnection))
1869
James E. Blaire511d2f2016-12-08 15:22:26 -08001870 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001871 # TODO(jhesketh): This should come from lib.connections for better
1872 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001873 # Register connections from the config
1874 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001875
Joshua Hesketh352264b2015-08-11 23:42:08 +10001876 def FakeSMTPFactory(*args, **kw):
1877 args = [self.smtp_messages] + list(args)
1878 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001879
Joshua Hesketh352264b2015-08-11 23:42:08 +10001880 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001881
James E. Blaire511d2f2016-12-08 15:22:26 -08001882 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001883 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001884 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001885
James E. Blair83005782015-12-11 14:46:03 -08001886 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001887 # This creates the per-test configuration object. It can be
1888 # overriden by subclasses, but should not need to be since it
1889 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001890 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001891 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001892
1893 if not self.setupSimpleLayout():
1894 if hasattr(self, 'tenant_config_file'):
1895 self.config.set('zuul', 'tenant_config',
1896 self.tenant_config_file)
1897 git_path = os.path.join(
1898 os.path.dirname(
1899 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1900 'git')
1901 if os.path.exists(git_path):
1902 for reponame in os.listdir(git_path):
1903 project = reponame.replace('_', '/')
1904 self.copyDirToRepo(project,
1905 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001906 self.setupAllProjectKeys()
1907
James E. Blair06cc3922017-04-19 10:08:10 -07001908 def setupSimpleLayout(self):
1909 # If the test method has been decorated with a simple_layout,
1910 # use that instead of the class tenant_config_file. Set up a
1911 # single config-project with the specified layout, and
1912 # initialize repos for all of the 'project' entries which
1913 # appear in the layout.
1914 test_name = self.id().split('.')[-1]
1915 test = getattr(self, test_name)
1916 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001917 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001918 else:
1919 return False
1920
James E. Blairb70e55a2017-04-19 12:57:02 -07001921 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001922 path = os.path.join(FIXTURE_DIR, path)
1923 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001924 data = f.read()
1925 layout = yaml.safe_load(data)
1926 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001927 untrusted_projects = []
1928 for item in layout:
1929 if 'project' in item:
1930 name = item['project']['name']
1931 untrusted_projects.append(name)
1932 self.init_repo(name)
1933 self.addCommitToRepo(name, 'initial commit',
1934 files={'README': ''},
1935 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001936 if 'job' in item:
1937 jobname = item['job']['name']
1938 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001939
1940 root = os.path.join(self.test_root, "config")
1941 if not os.path.exists(root):
1942 os.makedirs(root)
1943 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1944 config = [{'tenant':
1945 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001946 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001947 {'config-projects': ['common-config'],
1948 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001949 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001950 f.close()
1951 self.config.set('zuul', 'tenant_config',
1952 os.path.join(FIXTURE_DIR, f.name))
1953
1954 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001955 self.addCommitToRepo('common-config', 'add content from fixture',
1956 files, branch='master', tag='init')
1957
1958 return True
1959
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001960 def setupAllProjectKeys(self):
1961 if self.create_project_keys:
1962 return
1963
1964 path = self.config.get('zuul', 'tenant_config')
1965 with open(os.path.join(FIXTURE_DIR, path)) as f:
1966 tenant_config = yaml.safe_load(f.read())
1967 for tenant in tenant_config:
1968 sources = tenant['tenant']['source']
1969 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001970 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001971 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001972 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001973 self.setupProjectKeys(source, project)
1974
1975 def setupProjectKeys(self, source, project):
1976 # Make sure we set up an RSA key for the project so that we
1977 # don't spend time generating one:
1978
1979 key_root = os.path.join(self.state_root, 'keys')
1980 if not os.path.isdir(key_root):
1981 os.mkdir(key_root, 0o700)
1982 private_key_file = os.path.join(key_root, source, project + '.pem')
1983 private_key_dir = os.path.dirname(private_key_file)
1984 self.log.debug("Installing test keys for project %s at %s" % (
1985 project, private_key_file))
1986 if not os.path.isdir(private_key_dir):
1987 os.makedirs(private_key_dir)
1988 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1989 with open(private_key_file, 'w') as o:
1990 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001991
James E. Blair498059b2016-12-20 13:50:13 -08001992 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001993 self.zk_chroot_fixture = self.useFixture(
1994 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001995 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001996 self.zk_chroot_fixture.zookeeper_host,
1997 self.zk_chroot_fixture.zookeeper_port,
1998 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001999
James E. Blair96c6bf82016-01-15 16:20:40 -08002000 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002001 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002002
2003 files = {}
2004 for (dirpath, dirnames, filenames) in os.walk(source_path):
2005 for filename in filenames:
2006 test_tree_filepath = os.path.join(dirpath, filename)
2007 common_path = os.path.commonprefix([test_tree_filepath,
2008 source_path])
2009 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2010 with open(test_tree_filepath, 'r') as f:
2011 content = f.read()
2012 files[relative_filepath] = content
2013 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002014 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002015
James E. Blaire18d4602017-01-05 11:17:28 -08002016 def assertNodepoolState(self):
2017 # Make sure that there are no pending requests
2018
2019 requests = self.fake_nodepool.getNodeRequests()
2020 self.assertEqual(len(requests), 0)
2021
2022 nodes = self.fake_nodepool.getNodes()
2023 for node in nodes:
2024 self.assertFalse(node['_lock'], "Node %s is locked" %
2025 (node['_oid'],))
2026
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002027 def assertNoGeneratedKeys(self):
2028 # Make sure that Zuul did not generate any project keys
2029 # (unless it was supposed to).
2030
2031 if self.create_project_keys:
2032 return
2033
2034 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2035 test_key = i.read()
2036
2037 key_root = os.path.join(self.state_root, 'keys')
2038 for root, dirname, files in os.walk(key_root):
2039 for fn in files:
2040 with open(os.path.join(root, fn)) as f:
2041 self.assertEqual(test_key, f.read())
2042
Clark Boylanb640e052014-04-03 16:41:46 -07002043 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002044 self.log.debug("Assert final state")
2045 # Make sure no jobs are running
2046 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002047 # Make sure that git.Repo objects have been garbage collected.
2048 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002049 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002050 gc.collect()
2051 for obj in gc.get_objects():
2052 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002053 self.log.debug("Leaked git repo object: 0x%x %s" %
2054 (id(obj), repr(obj)))
2055 for ref in gc.get_referrers(obj):
2056 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002057 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002058 if repos:
2059 for obj in gc.garbage:
2060 self.log.debug(" Garbage %s" % (repr(obj)))
2061 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002062 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)