blob: 5e084fe23c82660f548c6ff2e5be5ed5c1c30cf9 [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
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
557 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700558 """Creates a new PR with several commits.
559 Sends an event about opened PR."""
560 self.github = github
561 self.source = github
562 self.number = number
563 self.project = project
564 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100565 self.subject = subject
566 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700567 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100570 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100571 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800572 self.reviews = []
573 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 self.updated_at = None
575 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100576 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100577 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700578 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100585 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 self._updateTimeStamp()
587
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100590 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700591 self._updateTimeStamp()
592
593 def getPullRequestOpenedEvent(self):
594 return self._getPullRequestEvent('opened')
595
596 def getPullRequestSynchronizeEvent(self):
597 return self._getPullRequestEvent('synchronize')
598
599 def getPullRequestReopenedEvent(self):
600 return self._getPullRequestEvent('reopened')
601
602 def getPullRequestClosedEvent(self):
603 return self._getPullRequestEvent('closed')
604
605 def addComment(self, message):
606 self.comments.append(message)
607 self._updateTimeStamp()
608
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200609 def getCommentAddedEvent(self, text):
610 name = 'issue_comment'
611 data = {
612 'action': 'created',
613 'issue': {
614 'number': self.number
615 },
616 'comment': {
617 'body': text
618 },
619 'repository': {
620 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100621 },
622 'sender': {
623 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200624 }
625 }
626 return (name, data)
627
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800628 def getReviewAddedEvent(self, review):
629 name = 'pull_request_review'
630 data = {
631 'action': 'submitted',
632 'pull_request': {
633 'number': self.number,
634 'title': self.subject,
635 'updated_at': self.updated_at,
636 'base': {
637 'ref': self.branch,
638 'repo': {
639 'full_name': self.project
640 }
641 },
642 'head': {
643 'sha': self.head_sha
644 }
645 },
646 'review': {
647 'state': review
648 },
649 'repository': {
650 'full_name': self.project
651 },
652 'sender': {
653 'login': 'ghuser'
654 }
655 }
656 return (name, data)
657
Jan Hruban16ad31f2015-11-07 14:39:07 +0100658 def addLabel(self, name):
659 if name not in self.labels:
660 self.labels.append(name)
661 self._updateTimeStamp()
662 return self._getLabelEvent(name)
663
664 def removeLabel(self, name):
665 if name in self.labels:
666 self.labels.remove(name)
667 self._updateTimeStamp()
668 return self._getUnlabelEvent(name)
669
670 def _getLabelEvent(self, label):
671 name = 'pull_request'
672 data = {
673 'action': 'labeled',
674 'pull_request': {
675 'number': self.number,
676 'updated_at': self.updated_at,
677 'base': {
678 'ref': self.branch,
679 'repo': {
680 'full_name': self.project
681 }
682 },
683 'head': {
684 'sha': self.head_sha
685 }
686 },
687 'label': {
688 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100689 },
690 'sender': {
691 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100692 }
693 }
694 return (name, data)
695
696 def _getUnlabelEvent(self, label):
697 name = 'pull_request'
698 data = {
699 'action': 'unlabeled',
700 'pull_request': {
701 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100702 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100703 'updated_at': self.updated_at,
704 'base': {
705 'ref': self.branch,
706 'repo': {
707 'full_name': self.project
708 }
709 },
710 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800711 'sha': self.head_sha,
712 'repo': {
713 'full_name': self.project
714 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100715 }
716 },
717 'label': {
718 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100719 },
720 'sender': {
721 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100722 }
723 }
724 return (name, data)
725
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 def _getRepo(self):
727 repo_path = os.path.join(self.upstream_root, self.project)
728 return git.Repo(repo_path)
729
730 def _createPRRef(self):
731 repo = self._getRepo()
732 GithubChangeReference.create(
733 repo, self._getPRReference(), 'refs/tags/init')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700736 repo = self._getRepo()
737 ref = repo.references[self._getPRReference()]
738 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100739 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700740 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100741 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 repo.head.reference = ref
743 zuul.merger.merger.reset_repo_to_head(repo)
744 repo.git.clean('-x', '-f', '-d')
745
Jan Hruban570d01c2016-03-10 21:51:32 +0100746 if files:
747 fn = files[0]
748 self.files = files
749 else:
750 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
751 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100752 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700753 fn = os.path.join(repo.working_dir, fn)
754 f = open(fn, 'w')
755 with open(fn, 'w') as f:
756 f.write("test %s %s\n" %
757 (self.branch, self.number))
758 repo.index.add([fn])
759
760 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800761 # Create an empty set of statuses for the given sha,
762 # each sha on a PR may have a status set on it
763 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700764 repo.head.reference = 'master'
765 zuul.merger.merger.reset_repo_to_head(repo)
766 repo.git.clean('-x', '-f', '-d')
767 repo.heads['master'].checkout()
768
769 def _updateTimeStamp(self):
770 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
771
772 def getPRHeadSha(self):
773 repo = self._getRepo()
774 return repo.references[self._getPRReference()].commit.hexsha
775
Jesse Keatingae4cd272017-01-30 17:10:44 -0800776 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800777 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
778 # convert the timestamp to a str format that would be returned
779 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800780
Adam Gandelmand81dd762017-02-09 15:15:49 -0800781 if granted_on:
782 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
783 submitted_at = time.strftime(
784 gh_time_format, granted_on.timetuple())
785 else:
786 # github timestamps only down to the second, so we need to make
787 # sure reviews that tests add appear to be added over a period of
788 # time in the past and not all at once.
789 if not self.reviews:
790 # the first review happens 10 mins ago
791 offset = 600
792 else:
793 # subsequent reviews happen 1 minute closer to now
794 offset = 600 - (len(self.reviews) * 60)
795
796 granted_on = datetime.datetime.utcfromtimestamp(
797 time.time() - offset)
798 submitted_at = time.strftime(
799 gh_time_format, granted_on.timetuple())
800
Jesse Keatingae4cd272017-01-30 17:10:44 -0800801 self.reviews.append({
802 'state': state,
803 'user': {
804 'login': user,
805 'email': user + "@derp.com",
806 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800807 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800808 })
809
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 def _getPRReference(self):
811 return '%s/head' % self.number
812
813 def _getPullRequestEvent(self, action):
814 name = 'pull_request'
815 data = {
816 'action': action,
817 'number': self.number,
818 'pull_request': {
819 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100820 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700821 'updated_at': self.updated_at,
822 'base': {
823 'ref': self.branch,
824 'repo': {
825 'full_name': self.project
826 }
827 },
828 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800829 'sha': self.head_sha,
830 'repo': {
831 'full_name': self.project
832 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700833 }
Jan Hruban3b415922016-02-03 13:10:22 +0100834 },
835 'sender': {
836 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 }
838 }
839 return (name, data)
840
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800841 def getCommitStatusEvent(self, context, state='success', user='zuul'):
842 name = 'status'
843 data = {
844 'state': state,
845 'sha': self.head_sha,
846 'description': 'Test results for %s: %s' % (self.head_sha, state),
847 'target_url': 'http://zuul/%s' % self.head_sha,
848 'branches': [],
849 'context': context,
850 'sender': {
851 'login': user
852 }
853 }
854 return (name, data)
855
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856
857class FakeGithubConnection(githubconnection.GithubConnection):
858 log = logging.getLogger("zuul.test.FakeGithubConnection")
859
860 def __init__(self, driver, connection_name, connection_config,
861 upstream_root=None):
862 super(FakeGithubConnection, self).__init__(driver, connection_name,
863 connection_config)
864 self.connection_name = connection_name
865 self.pr_number = 0
866 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700867 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700868 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100869 self.merge_failure = False
870 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100871 self.reports = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700872
Jan Hruban570d01c2016-03-10 21:51:32 +0100873 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874 self.pr_number += 1
875 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100876 self, self.pr_number, project, branch, subject, self.upstream_root,
877 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700878 self.pull_requests.append(pull_request)
879 return pull_request
880
Jesse Keating71a47ff2017-06-06 11:36:43 -0700881 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
882 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700883 if not old_rev:
884 old_rev = '00000000000000000000000000000000'
885 if not new_rev:
886 new_rev = random_sha1()
887 name = 'push'
888 data = {
889 'ref': ref,
890 'before': old_rev,
891 'after': new_rev,
892 'repository': {
893 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700894 },
895 'commits': [
896 {
897 'added': added_files,
898 'removed': removed_files,
899 'modified': modified_files
900 }
901 ]
Wayne1a78c612015-06-11 17:14:13 -0700902 }
903 return (name, data)
904
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 def emitEvent(self, event):
906 """Emulates sending the GitHub webhook event to the connection."""
907 port = self.webapp.server.socket.getsockname()[1]
908 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700909 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700910 headers = {'X-Github-Event': name}
911 req = urllib.request.Request(
912 'http://localhost:%s/connection/%s/payload'
913 % (port, self.connection_name),
914 data=payload, headers=headers)
915 urllib.request.urlopen(req)
916
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200917 def getPull(self, project, number):
918 pr = self.pull_requests[number - 1]
919 data = {
920 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100921 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200922 'updated_at': pr.updated_at,
923 'base': {
924 'repo': {
925 'full_name': pr.project
926 },
927 'ref': pr.branch,
928 },
Jan Hruban37615e52015-11-19 14:30:49 +0100929 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700930 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200931 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800932 'sha': pr.head_sha,
933 'repo': {
934 'full_name': pr.project
935 }
Jesse Keating61040e72017-06-08 15:08:27 -0700936 },
937 'files': pr.files
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 }
939 return data
940
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800941 def getPullBySha(self, sha):
942 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
943 if len(prs) > 1:
944 raise Exception('Multiple pulls found with head sha: %s' % sha)
945 pr = prs[0]
946 return self.getPull(pr.project, pr.number)
947
Jesse Keatingae4cd272017-01-30 17:10:44 -0800948 def _getPullReviews(self, owner, project, number):
949 pr = self.pull_requests[number - 1]
950 return pr.reviews
951
Jan Hruban3b415922016-02-03 13:10:22 +0100952 def getUser(self, login):
953 data = {
954 'username': login,
955 'name': 'Github User',
956 'email': 'github.user@example.com'
957 }
958 return data
959
Jesse Keatingae4cd272017-01-30 17:10:44 -0800960 def getRepoPermission(self, project, login):
961 owner, proj = project.split('/')
962 for pr in self.pull_requests:
963 pr_owner, pr_project = pr.project.split('/')
964 if (pr_owner == owner and proj == pr_project):
965 if login in pr.writers:
966 return 'write'
967 else:
968 return 'read'
969
Gregory Haynes4fc12542015-04-22 20:38:06 -0700970 def getGitUrl(self, project):
971 return os.path.join(self.upstream_root, str(project))
972
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200973 def real_getGitUrl(self, project):
974 return super(FakeGithubConnection, self).getGitUrl(project)
975
Gregory Haynes4fc12542015-04-22 20:38:06 -0700976 def getProjectBranches(self, project):
977 """Masks getProjectBranches since we don't have a real github"""
978
979 # just returns master for now
980 return ['master']
981
Jan Hrubane252a732017-01-03 15:03:09 +0100982 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +0100983 # record that this got reported
984 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -0700985 pull_request = self.pull_requests[pr_number - 1]
986 pull_request.addComment(message)
987
Jan Hruban3b415922016-02-03 13:10:22 +0100988 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +0100989 # record that this got reported
990 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +0100991 pull_request = self.pull_requests[pr_number - 1]
992 if self.merge_failure:
993 raise Exception('Pull request was not merged')
994 if self.merge_not_allowed_count > 0:
995 self.merge_not_allowed_count -= 1
996 raise MergeFailure('Merge was not successful due to mergeability'
997 ' conflict')
998 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100999 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001000
Jesse Keatingd96e5882017-01-19 13:55:50 -08001001 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001002 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001003
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001004 def setCommitStatus(self, project, sha, state, url='', description='',
1005 context='default', user='zuul'):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001006 # record that this got reported
1007 self.reports.append((project, sha, 'status', (user, context, state)))
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001008 # always insert a status to the front of the list, to represent
1009 # the last status provided for a commit.
1010 # Since we're bypassing github API, which would require a user, we
1011 # default the user as 'zuul' here.
1012 self.statuses.setdefault(project, {}).setdefault(sha, [])
1013 self.statuses[project][sha].insert(0, {
1014 'state': state,
1015 'url': url,
1016 'description': description,
1017 'context': context,
1018 'creator': {
1019 'login': user
1020 }
1021 })
Jan Hrubane252a732017-01-03 15:03:09 +01001022
Jan Hruban16ad31f2015-11-07 14:39:07 +01001023 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001024 # record that this got reported
1025 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001026 pull_request = self.pull_requests[pr_number - 1]
1027 pull_request.addLabel(label)
1028
1029 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001030 # record that this got reported
1031 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001032 pull_request = self.pull_requests[pr_number - 1]
1033 pull_request.removeLabel(label)
1034
Gregory Haynes4fc12542015-04-22 20:38:06 -07001035
Clark Boylanb640e052014-04-03 16:41:46 -07001036class BuildHistory(object):
1037 def __init__(self, **kw):
1038 self.__dict__.update(kw)
1039
1040 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001041 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1042 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001043
1044
Clark Boylanb640e052014-04-03 16:41:46 -07001045class FakeStatsd(threading.Thread):
1046 def __init__(self):
1047 threading.Thread.__init__(self)
1048 self.daemon = True
1049 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1050 self.sock.bind(('', 0))
1051 self.port = self.sock.getsockname()[1]
1052 self.wake_read, self.wake_write = os.pipe()
1053 self.stats = []
1054
1055 def run(self):
1056 while True:
1057 poll = select.poll()
1058 poll.register(self.sock, select.POLLIN)
1059 poll.register(self.wake_read, select.POLLIN)
1060 ret = poll.poll()
1061 for (fd, event) in ret:
1062 if fd == self.sock.fileno():
1063 data = self.sock.recvfrom(1024)
1064 if not data:
1065 return
1066 self.stats.append(data[0])
1067 if fd == self.wake_read:
1068 return
1069
1070 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001071 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001072
1073
James E. Blaire1767bc2016-08-02 10:00:27 -07001074class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001075 log = logging.getLogger("zuul.test")
1076
Paul Belanger174a8272017-03-14 13:20:10 -04001077 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001078 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001079 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001080 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001081 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001082 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001083 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001084 # TODOv3(jeblair): self.node is really "the label of the node
1085 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001086 # keep using it like this, or we may end up exposing more of
1087 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001088 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001089 self.node = None
1090 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001091 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001092 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001093 self.pipeline = self.parameters['ZUUL_PIPELINE']
1094 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001095 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001096 self.wait_condition = threading.Condition()
1097 self.waiting = False
1098 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001099 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001100 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001101 self.changes = None
1102 if 'ZUUL_CHANGE_IDS' in self.parameters:
1103 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001104
James E. Blair3158e282016-08-19 09:34:11 -07001105 def __repr__(self):
1106 waiting = ''
1107 if self.waiting:
1108 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001109 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1110 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001111
Clark Boylanb640e052014-04-03 16:41:46 -07001112 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001113 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001114 self.wait_condition.acquire()
1115 self.wait_condition.notify()
1116 self.waiting = False
1117 self.log.debug("Build %s released" % self.unique)
1118 self.wait_condition.release()
1119
1120 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001121 """Return whether this build is being held.
1122
1123 :returns: Whether the build is being held.
1124 :rtype: bool
1125 """
1126
Clark Boylanb640e052014-04-03 16:41:46 -07001127 self.wait_condition.acquire()
1128 if self.waiting:
1129 ret = True
1130 else:
1131 ret = False
1132 self.wait_condition.release()
1133 return ret
1134
1135 def _wait(self):
1136 self.wait_condition.acquire()
1137 self.waiting = True
1138 self.log.debug("Build %s waiting" % self.unique)
1139 self.wait_condition.wait()
1140 self.wait_condition.release()
1141
1142 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001143 self.log.debug('Running build %s' % self.unique)
1144
Paul Belanger174a8272017-03-14 13:20:10 -04001145 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001146 self.log.debug('Holding build %s' % self.unique)
1147 self._wait()
1148 self.log.debug("Build %s continuing" % self.unique)
1149
James E. Blair412fba82017-01-26 15:00:50 -08001150 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001151 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001152 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001153 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001154 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001155 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001156 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001157
James E. Blaire1767bc2016-08-02 10:00:27 -07001158 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001159
James E. Blaira5dba232016-08-08 15:53:24 -07001160 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001161 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001162 for change in changes:
1163 if self.hasChanges(change):
1164 return True
1165 return False
1166
James E. Blaire7b99a02016-08-05 14:27:34 -07001167 def hasChanges(self, *changes):
1168 """Return whether this build has certain changes in its git repos.
1169
1170 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001171 are expected to be present (in order) in the git repository of
1172 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001173
1174 :returns: Whether the build has the indicated changes.
1175 :rtype: bool
1176
1177 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001178 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001179 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001180 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001181 try:
1182 repo = git.Repo(path)
1183 except NoSuchPathError as e:
1184 self.log.debug('%s' % e)
1185 return False
1186 ref = self.parameters['ZUUL_REF']
1187 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1188 commit_message = '%s-1' % change.subject
1189 self.log.debug("Checking if build %s has changes; commit_message "
1190 "%s; repo_messages %s" % (self, commit_message,
1191 repo_messages))
1192 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001193 self.log.debug(" messages do not match")
1194 return False
1195 self.log.debug(" OK")
1196 return True
1197
James E. Blaird8af5422017-05-24 13:59:40 -07001198 def getWorkspaceRepos(self, projects):
1199 """Return workspace git repo objects for the listed projects
1200
1201 :arg list projects: A list of strings, each the canonical name
1202 of a project.
1203
1204 :returns: A dictionary of {name: repo} for every listed
1205 project.
1206 :rtype: dict
1207
1208 """
1209
1210 repos = {}
1211 for project in projects:
1212 path = os.path.join(self.jobdir.src_root, project)
1213 repo = git.Repo(path)
1214 repos[project] = repo
1215 return repos
1216
Clark Boylanb640e052014-04-03 16:41:46 -07001217
Paul Belanger174a8272017-03-14 13:20:10 -04001218class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1219 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001220
Paul Belanger174a8272017-03-14 13:20:10 -04001221 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001222 they will report that they have started but then pause until
1223 released before reporting completion. This attribute may be
1224 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001225 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001226 be explicitly released.
1227
1228 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001229 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001230 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001231 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001232 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001233 self.hold_jobs_in_build = False
1234 self.lock = threading.Lock()
1235 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001236 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001237 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001238 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001239 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001240
James E. Blaira5dba232016-08-08 15:53:24 -07001241 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001242 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001243
1244 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001245 :arg Change change: The :py:class:`~tests.base.FakeChange`
1246 instance which should cause the job to fail. This job
1247 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001248
1249 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001250 l = self.fail_tests.get(name, [])
1251 l.append(change)
1252 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001253
James E. Blair962220f2016-08-03 11:22:38 -07001254 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001255 """Release a held build.
1256
1257 :arg str regex: A regular expression which, if supplied, will
1258 cause only builds with matching names to be released. If
1259 not supplied, all builds will be released.
1260
1261 """
James E. Blair962220f2016-08-03 11:22:38 -07001262 builds = self.running_builds[:]
1263 self.log.debug("Releasing build %s (%s)" % (regex,
1264 len(self.running_builds)))
1265 for build in builds:
1266 if not regex or re.match(regex, build.name):
1267 self.log.debug("Releasing build %s" %
1268 (build.parameters['ZUUL_UUID']))
1269 build.release()
1270 else:
1271 self.log.debug("Not releasing build %s" %
1272 (build.parameters['ZUUL_UUID']))
1273 self.log.debug("Done releasing builds %s (%s)" %
1274 (regex, len(self.running_builds)))
1275
Paul Belanger174a8272017-03-14 13:20:10 -04001276 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001277 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001278 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001279 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001280 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001281 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001282 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001283 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001284 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1285 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001286
1287 def stopJob(self, job):
1288 self.log.debug("handle stop")
1289 parameters = json.loads(job.arguments)
1290 uuid = parameters['uuid']
1291 for build in self.running_builds:
1292 if build.unique == uuid:
1293 build.aborted = True
1294 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001295 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001296
James E. Blaira002b032017-04-18 10:35:48 -07001297 def stop(self):
1298 for build in self.running_builds:
1299 build.release()
1300 super(RecordingExecutorServer, self).stop()
1301
Joshua Hesketh50c21782016-10-13 21:34:14 +11001302
Paul Belanger174a8272017-03-14 13:20:10 -04001303class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001304 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001305 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001306 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001307 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001308 if not commit: # merge conflict
1309 self.recordResult('MERGER_FAILURE')
1310 return commit
1311
1312 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001313 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001314 self.executor_server.lock.acquire()
1315 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001316 BuildHistory(name=build.name, result=result, changes=build.changes,
1317 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001318 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001319 pipeline=build.parameters['ZUUL_PIPELINE'])
1320 )
Paul Belanger174a8272017-03-14 13:20:10 -04001321 self.executor_server.running_builds.remove(build)
1322 del self.executor_server.job_builds[self.job.unique]
1323 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001324
1325 def runPlaybooks(self, args):
1326 build = self.executor_server.job_builds[self.job.unique]
1327 build.jobdir = self.jobdir
1328
1329 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1330 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001331 return result
1332
Monty Taylore6562aa2017-02-20 07:37:39 -05001333 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001334 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001335
Paul Belanger174a8272017-03-14 13:20:10 -04001336 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001337 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001338 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001339 else:
1340 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001341 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001342
James E. Blairad8dca02017-02-21 11:48:32 -05001343 def getHostList(self, args):
1344 self.log.debug("hostlist")
1345 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001346 for host in hosts:
1347 host['host_vars']['ansible_connection'] = 'local'
1348
1349 hosts.append(dict(
1350 name='localhost',
1351 host_vars=dict(ansible_connection='local'),
1352 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001353 return hosts
1354
James E. Blairf5dbd002015-12-23 15:26:17 -08001355
Clark Boylanb640e052014-04-03 16:41:46 -07001356class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001357 """A Gearman server for use in tests.
1358
1359 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1360 added to the queue but will not be distributed to workers
1361 until released. This attribute may be changed at any time and
1362 will take effect for subsequently enqueued jobs, but
1363 previously held jobs will still need to be explicitly
1364 released.
1365
1366 """
1367
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001368 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001369 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001370 if use_ssl:
1371 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1372 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1373 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1374 else:
1375 ssl_ca = None
1376 ssl_cert = None
1377 ssl_key = None
1378
1379 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1380 ssl_cert=ssl_cert,
1381 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001382
1383 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001384 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1385 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001386 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001387 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001388 job.waiting = self.hold_jobs_in_queue
1389 else:
1390 job.waiting = False
1391 if job.waiting:
1392 continue
1393 if job.name in connection.functions:
1394 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001395 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001396 connection.related_jobs[job.handle] = job
1397 job.worker_connection = connection
1398 job.running = True
1399 return job
1400 return None
1401
1402 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001403 """Release a held job.
1404
1405 :arg str regex: A regular expression which, if supplied, will
1406 cause only jobs with matching names to be released. If
1407 not supplied, all jobs will be released.
1408 """
Clark Boylanb640e052014-04-03 16:41:46 -07001409 released = False
1410 qlen = (len(self.high_queue) + len(self.normal_queue) +
1411 len(self.low_queue))
1412 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1413 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001414 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001415 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001416 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001417 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001418 self.log.debug("releasing queued job %s" %
1419 job.unique)
1420 job.waiting = False
1421 released = True
1422 else:
1423 self.log.debug("not releasing queued job %s" %
1424 job.unique)
1425 if released:
1426 self.wakeConnections()
1427 qlen = (len(self.high_queue) + len(self.normal_queue) +
1428 len(self.low_queue))
1429 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1430
1431
1432class FakeSMTP(object):
1433 log = logging.getLogger('zuul.FakeSMTP')
1434
1435 def __init__(self, messages, server, port):
1436 self.server = server
1437 self.port = port
1438 self.messages = messages
1439
1440 def sendmail(self, from_email, to_email, msg):
1441 self.log.info("Sending email from %s, to %s, with msg %s" % (
1442 from_email, to_email, msg))
1443
1444 headers = msg.split('\n\n', 1)[0]
1445 body = msg.split('\n\n', 1)[1]
1446
1447 self.messages.append(dict(
1448 from_email=from_email,
1449 to_email=to_email,
1450 msg=msg,
1451 headers=headers,
1452 body=body,
1453 ))
1454
1455 return True
1456
1457 def quit(self):
1458 return True
1459
1460
James E. Blairdce6cea2016-12-20 16:45:32 -08001461class FakeNodepool(object):
1462 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001463 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001464
1465 log = logging.getLogger("zuul.test.FakeNodepool")
1466
1467 def __init__(self, host, port, chroot):
1468 self.client = kazoo.client.KazooClient(
1469 hosts='%s:%s%s' % (host, port, chroot))
1470 self.client.start()
1471 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001472 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001473 self.thread = threading.Thread(target=self.run)
1474 self.thread.daemon = True
1475 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001476 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001477
1478 def stop(self):
1479 self._running = False
1480 self.thread.join()
1481 self.client.stop()
1482 self.client.close()
1483
1484 def run(self):
1485 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001486 try:
1487 self._run()
1488 except Exception:
1489 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001490 time.sleep(0.1)
1491
1492 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001493 if self.paused:
1494 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001495 for req in self.getNodeRequests():
1496 self.fulfillRequest(req)
1497
1498 def getNodeRequests(self):
1499 try:
1500 reqids = self.client.get_children(self.REQUEST_ROOT)
1501 except kazoo.exceptions.NoNodeError:
1502 return []
1503 reqs = []
1504 for oid in sorted(reqids):
1505 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001506 try:
1507 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001508 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001509 data['_oid'] = oid
1510 reqs.append(data)
1511 except kazoo.exceptions.NoNodeError:
1512 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001513 return reqs
1514
James E. Blaire18d4602017-01-05 11:17:28 -08001515 def getNodes(self):
1516 try:
1517 nodeids = self.client.get_children(self.NODE_ROOT)
1518 except kazoo.exceptions.NoNodeError:
1519 return []
1520 nodes = []
1521 for oid in sorted(nodeids):
1522 path = self.NODE_ROOT + '/' + oid
1523 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001524 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001525 data['_oid'] = oid
1526 try:
1527 lockfiles = self.client.get_children(path + '/lock')
1528 except kazoo.exceptions.NoNodeError:
1529 lockfiles = []
1530 if lockfiles:
1531 data['_lock'] = True
1532 else:
1533 data['_lock'] = False
1534 nodes.append(data)
1535 return nodes
1536
James E. Blaira38c28e2017-01-04 10:33:20 -08001537 def makeNode(self, request_id, node_type):
1538 now = time.time()
1539 path = '/nodepool/nodes/'
1540 data = dict(type=node_type,
1541 provider='test-provider',
1542 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001543 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001544 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001545 public_ipv4='127.0.0.1',
1546 private_ipv4=None,
1547 public_ipv6=None,
1548 allocated_to=request_id,
1549 state='ready',
1550 state_time=now,
1551 created_time=now,
1552 updated_time=now,
1553 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001554 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001555 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001556 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001557 path = self.client.create(path, data,
1558 makepath=True,
1559 sequence=True)
1560 nodeid = path.split("/")[-1]
1561 return nodeid
1562
James E. Blair6ab79e02017-01-06 10:10:17 -08001563 def addFailRequest(self, request):
1564 self.fail_requests.add(request['_oid'])
1565
James E. Blairdce6cea2016-12-20 16:45:32 -08001566 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001567 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001568 return
1569 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001570 oid = request['_oid']
1571 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001572
James E. Blair6ab79e02017-01-06 10:10:17 -08001573 if oid in self.fail_requests:
1574 request['state'] = 'failed'
1575 else:
1576 request['state'] = 'fulfilled'
1577 nodes = []
1578 for node in request['node_types']:
1579 nodeid = self.makeNode(oid, node)
1580 nodes.append(nodeid)
1581 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001582
James E. Blaira38c28e2017-01-04 10:33:20 -08001583 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001584 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001585 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001586 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001587 try:
1588 self.client.set(path, data)
1589 except kazoo.exceptions.NoNodeError:
1590 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001591
1592
James E. Blair498059b2016-12-20 13:50:13 -08001593class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001594 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001595 super(ChrootedKazooFixture, self).__init__()
1596
1597 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1598 if ':' in zk_host:
1599 host, port = zk_host.split(':')
1600 else:
1601 host = zk_host
1602 port = None
1603
1604 self.zookeeper_host = host
1605
1606 if not port:
1607 self.zookeeper_port = 2181
1608 else:
1609 self.zookeeper_port = int(port)
1610
Clark Boylan621ec9a2017-04-07 17:41:33 -07001611 self.test_id = test_id
1612
James E. Blair498059b2016-12-20 13:50:13 -08001613 def _setUp(self):
1614 # Make sure the test chroot paths do not conflict
1615 random_bits = ''.join(random.choice(string.ascii_lowercase +
1616 string.ascii_uppercase)
1617 for x in range(8))
1618
Clark Boylan621ec9a2017-04-07 17:41:33 -07001619 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001620 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1621
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001622 self.addCleanup(self._cleanup)
1623
James E. Blair498059b2016-12-20 13:50:13 -08001624 # Ensure the chroot path exists and clean up any pre-existing znodes.
1625 _tmp_client = kazoo.client.KazooClient(
1626 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1627 _tmp_client.start()
1628
1629 if _tmp_client.exists(self.zookeeper_chroot):
1630 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1631
1632 _tmp_client.ensure_path(self.zookeeper_chroot)
1633 _tmp_client.stop()
1634 _tmp_client.close()
1635
James E. Blair498059b2016-12-20 13:50:13 -08001636 def _cleanup(self):
1637 '''Remove the chroot path.'''
1638 # Need a non-chroot'ed client to remove the chroot path
1639 _tmp_client = kazoo.client.KazooClient(
1640 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1641 _tmp_client.start()
1642 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1643 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001644 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001645
1646
Joshua Heskethd78b4482015-09-14 16:56:34 -06001647class MySQLSchemaFixture(fixtures.Fixture):
1648 def setUp(self):
1649 super(MySQLSchemaFixture, self).setUp()
1650
1651 random_bits = ''.join(random.choice(string.ascii_lowercase +
1652 string.ascii_uppercase)
1653 for x in range(8))
1654 self.name = '%s_%s' % (random_bits, os.getpid())
1655 self.passwd = uuid.uuid4().hex
1656 db = pymysql.connect(host="localhost",
1657 user="openstack_citest",
1658 passwd="openstack_citest",
1659 db="openstack_citest")
1660 cur = db.cursor()
1661 cur.execute("create database %s" % self.name)
1662 cur.execute(
1663 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1664 (self.name, self.name, self.passwd))
1665 cur.execute("flush privileges")
1666
1667 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1668 self.passwd,
1669 self.name)
1670 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1671 self.addCleanup(self.cleanup)
1672
1673 def cleanup(self):
1674 db = pymysql.connect(host="localhost",
1675 user="openstack_citest",
1676 passwd="openstack_citest",
1677 db="openstack_citest")
1678 cur = db.cursor()
1679 cur.execute("drop database %s" % self.name)
1680 cur.execute("drop user '%s'@'localhost'" % self.name)
1681 cur.execute("flush privileges")
1682
1683
Maru Newby3fe5f852015-01-13 04:22:14 +00001684class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001685 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001686 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001687
James E. Blair1c236df2017-02-01 14:07:24 -08001688 def attachLogs(self, *args):
1689 def reader():
1690 self._log_stream.seek(0)
1691 while True:
1692 x = self._log_stream.read(4096)
1693 if not x:
1694 break
1695 yield x.encode('utf8')
1696 content = testtools.content.content_from_reader(
1697 reader,
1698 testtools.content_type.UTF8_TEXT,
1699 False)
1700 self.addDetail('logging', content)
1701
Clark Boylanb640e052014-04-03 16:41:46 -07001702 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001703 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001704 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1705 try:
1706 test_timeout = int(test_timeout)
1707 except ValueError:
1708 # If timeout value is invalid do not set a timeout.
1709 test_timeout = 0
1710 if test_timeout > 0:
1711 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1712
1713 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1714 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1715 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1716 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1717 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1718 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1719 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1720 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1721 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1722 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001723 self._log_stream = StringIO()
1724 self.addOnException(self.attachLogs)
1725 else:
1726 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001727
James E. Blair73b41772017-05-22 13:22:55 -07001728 # NOTE(jeblair): this is temporary extra debugging to try to
1729 # track down a possible leak.
1730 orig_git_repo_init = git.Repo.__init__
1731
1732 def git_repo_init(myself, *args, **kw):
1733 orig_git_repo_init(myself, *args, **kw)
1734 self.log.debug("Created git repo 0x%x %s" %
1735 (id(myself), repr(myself)))
1736
1737 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1738 git_repo_init))
1739
James E. Blair1c236df2017-02-01 14:07:24 -08001740 handler = logging.StreamHandler(self._log_stream)
1741 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1742 '%(levelname)-8s %(message)s')
1743 handler.setFormatter(formatter)
1744
1745 logger = logging.getLogger()
1746 logger.setLevel(logging.DEBUG)
1747 logger.addHandler(handler)
1748
Clark Boylan3410d532017-04-25 12:35:29 -07001749 # Make sure we don't carry old handlers around in process state
1750 # which slows down test runs
1751 self.addCleanup(logger.removeHandler, handler)
1752 self.addCleanup(handler.close)
1753 self.addCleanup(handler.flush)
1754
James E. Blair1c236df2017-02-01 14:07:24 -08001755 # NOTE(notmorgan): Extract logging overrides for specific
1756 # libraries from the OS_LOG_DEFAULTS env and create loggers
1757 # for each. This is used to limit the output during test runs
1758 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001759 log_defaults_from_env = os.environ.get(
1760 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001761 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001762
James E. Blairdce6cea2016-12-20 16:45:32 -08001763 if log_defaults_from_env:
1764 for default in log_defaults_from_env.split(','):
1765 try:
1766 name, level_str = default.split('=', 1)
1767 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001768 logger = logging.getLogger(name)
1769 logger.setLevel(level)
1770 logger.addHandler(handler)
1771 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001772 except ValueError:
1773 # NOTE(notmorgan): Invalid format of the log default,
1774 # skip and don't try and apply a logger for the
1775 # specified module
1776 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001777
Maru Newby3fe5f852015-01-13 04:22:14 +00001778
1779class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001780 """A test case with a functioning Zuul.
1781
1782 The following class variables are used during test setup and can
1783 be overidden by subclasses but are effectively read-only once a
1784 test method starts running:
1785
1786 :cvar str config_file: This points to the main zuul config file
1787 within the fixtures directory. Subclasses may override this
1788 to obtain a different behavior.
1789
1790 :cvar str tenant_config_file: This is the tenant config file
1791 (which specifies from what git repos the configuration should
1792 be loaded). It defaults to the value specified in
1793 `config_file` but can be overidden by subclasses to obtain a
1794 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001795 configuration. See also the :py:func:`simple_layout`
1796 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001797
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001798 :cvar bool create_project_keys: Indicates whether Zuul should
1799 auto-generate keys for each project, or whether the test
1800 infrastructure should insert dummy keys to save time during
1801 startup. Defaults to False.
1802
James E. Blaire7b99a02016-08-05 14:27:34 -07001803 The following are instance variables that are useful within test
1804 methods:
1805
1806 :ivar FakeGerritConnection fake_<connection>:
1807 A :py:class:`~tests.base.FakeGerritConnection` will be
1808 instantiated for each connection present in the config file
1809 and stored here. For instance, `fake_gerrit` will hold the
1810 FakeGerritConnection object for a connection named `gerrit`.
1811
1812 :ivar FakeGearmanServer gearman_server: An instance of
1813 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1814 server that all of the Zuul components in this test use to
1815 communicate with each other.
1816
Paul Belanger174a8272017-03-14 13:20:10 -04001817 :ivar RecordingExecutorServer executor_server: An instance of
1818 :py:class:`~tests.base.RecordingExecutorServer` which is the
1819 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001820
1821 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1822 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001823 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001824 list upon completion.
1825
1826 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1827 objects representing completed builds. They are appended to
1828 the list in the order they complete.
1829
1830 """
1831
James E. Blair83005782015-12-11 14:46:03 -08001832 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001833 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001834 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001835 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001836
1837 def _startMerger(self):
1838 self.merge_server = zuul.merger.server.MergeServer(self.config,
1839 self.connections)
1840 self.merge_server.start()
1841
Maru Newby3fe5f852015-01-13 04:22:14 +00001842 def setUp(self):
1843 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001844
1845 self.setupZK()
1846
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001847 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001848 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001849 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1850 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001851 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001852 tmp_root = tempfile.mkdtemp(
1853 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001854 self.test_root = os.path.join(tmp_root, "zuul-test")
1855 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001856 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001857 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001858 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001859
1860 if os.path.exists(self.test_root):
1861 shutil.rmtree(self.test_root)
1862 os.makedirs(self.test_root)
1863 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001864 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001865
1866 # Make per test copy of Configuration.
1867 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001868 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1869 if not os.path.exists(self.private_key_file):
1870 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1871 shutil.copy(src_private_key_file, self.private_key_file)
1872 shutil.copy('{}.pub'.format(src_private_key_file),
1873 '{}.pub'.format(self.private_key_file))
1874 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001875 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001876 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001877 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001878 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001879 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001880 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001881 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001882
Clark Boylanb640e052014-04-03 16:41:46 -07001883 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001884 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1885 # see: https://github.com/jsocol/pystatsd/issues/61
1886 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001887 os.environ['STATSD_PORT'] = str(self.statsd.port)
1888 self.statsd.start()
1889 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001890 importlib.reload(statsd)
1891 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001892
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001893 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001894
1895 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001896 self.log.info("Gearman server on port %s" %
1897 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001898 if self.use_ssl:
1899 self.log.info('SSL enabled for gearman')
1900 self.config.set(
1901 'gearman', 'ssl_ca',
1902 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1903 self.config.set(
1904 'gearman', 'ssl_cert',
1905 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1906 self.config.set(
1907 'gearman', 'ssl_key',
1908 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001909
James E. Blaire511d2f2016-12-08 15:22:26 -08001910 gerritsource.GerritSource.replication_timeout = 1.5
1911 gerritsource.GerritSource.replication_retry_interval = 0.5
1912 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001913
Joshua Hesketh352264b2015-08-11 23:42:08 +10001914 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001915
Jan Hruban7083edd2015-08-21 14:00:54 +02001916 self.webapp = zuul.webapp.WebApp(
1917 self.sched, port=0, listen_address='127.0.0.1')
1918
Jan Hruban6b71aff2015-10-22 16:58:08 +02001919 self.event_queues = [
1920 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001921 self.sched.trigger_event_queue,
1922 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001923 ]
1924
James E. Blairfef78942016-03-11 16:28:56 -08001925 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001926 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001927
Paul Belanger174a8272017-03-14 13:20:10 -04001928 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001929 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001930 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001931 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001932 _test_root=self.test_root,
1933 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001934 self.executor_server.start()
1935 self.history = self.executor_server.build_history
1936 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001937
Paul Belanger174a8272017-03-14 13:20:10 -04001938 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001939 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001940 self.merge_client = zuul.merger.client.MergeClient(
1941 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001942 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001943 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001944 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001945
James E. Blair0d5a36e2017-02-21 10:53:44 -05001946 self.fake_nodepool = FakeNodepool(
1947 self.zk_chroot_fixture.zookeeper_host,
1948 self.zk_chroot_fixture.zookeeper_port,
1949 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001950
Paul Belanger174a8272017-03-14 13:20:10 -04001951 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001952 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001953 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001954 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001955
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001956 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001957
1958 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001959 self.webapp.start()
1960 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001961 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001962 # Cleanups are run in reverse order
1963 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001964 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001965 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001966
James E. Blairb9c0d772017-03-03 14:34:49 -08001967 self.sched.reconfigure(self.config)
1968 self.sched.resume()
1969
Tobias Henkel7df274b2017-05-26 17:41:11 +02001970 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001971 # Set up gerrit related fakes
1972 # Set a changes database so multiple FakeGerrit's can report back to
1973 # a virtual canonical database given by the configured hostname
1974 self.gerrit_changes_dbs = {}
1975
1976 def getGerritConnection(driver, name, config):
1977 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1978 con = FakeGerritConnection(driver, name, config,
1979 changes_db=db,
1980 upstream_root=self.upstream_root)
1981 self.event_queues.append(con.event_queue)
1982 setattr(self, 'fake_' + name, con)
1983 return con
1984
1985 self.useFixture(fixtures.MonkeyPatch(
1986 'zuul.driver.gerrit.GerritDriver.getConnection',
1987 getGerritConnection))
1988
Gregory Haynes4fc12542015-04-22 20:38:06 -07001989 def getGithubConnection(driver, name, config):
1990 con = FakeGithubConnection(driver, name, config,
1991 upstream_root=self.upstream_root)
1992 setattr(self, 'fake_' + name, con)
1993 return con
1994
1995 self.useFixture(fixtures.MonkeyPatch(
1996 'zuul.driver.github.GithubDriver.getConnection',
1997 getGithubConnection))
1998
James E. Blaire511d2f2016-12-08 15:22:26 -08001999 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002000 # TODO(jhesketh): This should come from lib.connections for better
2001 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002002 # Register connections from the config
2003 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002004
Joshua Hesketh352264b2015-08-11 23:42:08 +10002005 def FakeSMTPFactory(*args, **kw):
2006 args = [self.smtp_messages] + list(args)
2007 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002008
Joshua Hesketh352264b2015-08-11 23:42:08 +10002009 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002010
James E. Blaire511d2f2016-12-08 15:22:26 -08002011 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002012 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002013 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002014
James E. Blair83005782015-12-11 14:46:03 -08002015 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002016 # This creates the per-test configuration object. It can be
2017 # overriden by subclasses, but should not need to be since it
2018 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002019 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002020 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002021
2022 if not self.setupSimpleLayout():
2023 if hasattr(self, 'tenant_config_file'):
2024 self.config.set('zuul', 'tenant_config',
2025 self.tenant_config_file)
2026 git_path = os.path.join(
2027 os.path.dirname(
2028 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2029 'git')
2030 if os.path.exists(git_path):
2031 for reponame in os.listdir(git_path):
2032 project = reponame.replace('_', '/')
2033 self.copyDirToRepo(project,
2034 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002035 self.setupAllProjectKeys()
2036
James E. Blair06cc3922017-04-19 10:08:10 -07002037 def setupSimpleLayout(self):
2038 # If the test method has been decorated with a simple_layout,
2039 # use that instead of the class tenant_config_file. Set up a
2040 # single config-project with the specified layout, and
2041 # initialize repos for all of the 'project' entries which
2042 # appear in the layout.
2043 test_name = self.id().split('.')[-1]
2044 test = getattr(self, test_name)
2045 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002046 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002047 else:
2048 return False
2049
James E. Blairb70e55a2017-04-19 12:57:02 -07002050 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002051 path = os.path.join(FIXTURE_DIR, path)
2052 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002053 data = f.read()
2054 layout = yaml.safe_load(data)
2055 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002056 untrusted_projects = []
2057 for item in layout:
2058 if 'project' in item:
2059 name = item['project']['name']
2060 untrusted_projects.append(name)
2061 self.init_repo(name)
2062 self.addCommitToRepo(name, 'initial commit',
2063 files={'README': ''},
2064 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002065 if 'job' in item:
2066 jobname = item['job']['name']
2067 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002068
2069 root = os.path.join(self.test_root, "config")
2070 if not os.path.exists(root):
2071 os.makedirs(root)
2072 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2073 config = [{'tenant':
2074 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002075 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002076 {'config-projects': ['common-config'],
2077 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002078 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002079 f.close()
2080 self.config.set('zuul', 'tenant_config',
2081 os.path.join(FIXTURE_DIR, f.name))
2082
2083 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002084 self.addCommitToRepo('common-config', 'add content from fixture',
2085 files, branch='master', tag='init')
2086
2087 return True
2088
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002089 def setupAllProjectKeys(self):
2090 if self.create_project_keys:
2091 return
2092
2093 path = self.config.get('zuul', 'tenant_config')
2094 with open(os.path.join(FIXTURE_DIR, path)) as f:
2095 tenant_config = yaml.safe_load(f.read())
2096 for tenant in tenant_config:
2097 sources = tenant['tenant']['source']
2098 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002099 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002100 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002101 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002102 self.setupProjectKeys(source, project)
2103
2104 def setupProjectKeys(self, source, project):
2105 # Make sure we set up an RSA key for the project so that we
2106 # don't spend time generating one:
2107
2108 key_root = os.path.join(self.state_root, 'keys')
2109 if not os.path.isdir(key_root):
2110 os.mkdir(key_root, 0o700)
2111 private_key_file = os.path.join(key_root, source, project + '.pem')
2112 private_key_dir = os.path.dirname(private_key_file)
2113 self.log.debug("Installing test keys for project %s at %s" % (
2114 project, private_key_file))
2115 if not os.path.isdir(private_key_dir):
2116 os.makedirs(private_key_dir)
2117 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2118 with open(private_key_file, 'w') as o:
2119 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002120
James E. Blair498059b2016-12-20 13:50:13 -08002121 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002122 self.zk_chroot_fixture = self.useFixture(
2123 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002124 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002125 self.zk_chroot_fixture.zookeeper_host,
2126 self.zk_chroot_fixture.zookeeper_port,
2127 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002128
James E. Blair96c6bf82016-01-15 16:20:40 -08002129 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002130 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002131
2132 files = {}
2133 for (dirpath, dirnames, filenames) in os.walk(source_path):
2134 for filename in filenames:
2135 test_tree_filepath = os.path.join(dirpath, filename)
2136 common_path = os.path.commonprefix([test_tree_filepath,
2137 source_path])
2138 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2139 with open(test_tree_filepath, 'r') as f:
2140 content = f.read()
2141 files[relative_filepath] = content
2142 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002143 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002144
James E. Blaire18d4602017-01-05 11:17:28 -08002145 def assertNodepoolState(self):
2146 # Make sure that there are no pending requests
2147
2148 requests = self.fake_nodepool.getNodeRequests()
2149 self.assertEqual(len(requests), 0)
2150
2151 nodes = self.fake_nodepool.getNodes()
2152 for node in nodes:
2153 self.assertFalse(node['_lock'], "Node %s is locked" %
2154 (node['_oid'],))
2155
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002156 def assertNoGeneratedKeys(self):
2157 # Make sure that Zuul did not generate any project keys
2158 # (unless it was supposed to).
2159
2160 if self.create_project_keys:
2161 return
2162
2163 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2164 test_key = i.read()
2165
2166 key_root = os.path.join(self.state_root, 'keys')
2167 for root, dirname, files in os.walk(key_root):
2168 for fn in files:
2169 with open(os.path.join(root, fn)) as f:
2170 self.assertEqual(test_key, f.read())
2171
Clark Boylanb640e052014-04-03 16:41:46 -07002172 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002173 self.log.debug("Assert final state")
2174 # Make sure no jobs are running
2175 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002176 # Make sure that git.Repo objects have been garbage collected.
2177 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002178 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002179 gc.collect()
2180 for obj in gc.get_objects():
2181 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002182 self.log.debug("Leaked git repo object: 0x%x %s" %
2183 (id(obj), repr(obj)))
2184 for ref in gc.get_referrers(obj):
2185 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002186 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002187 if repos:
2188 for obj in gc.garbage:
2189 self.log.debug(" Garbage %s" % (repr(obj)))
2190 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002191 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002192 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002193 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002194 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002195 for tenant in self.sched.abide.tenants.values():
2196 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002197 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002198 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002199
2200 def shutdown(self):
2201 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002202 self.executor_server.hold_jobs_in_build = False
2203 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002204 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002205 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002206 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002207 self.sched.stop()
2208 self.sched.join()
2209 self.statsd.stop()
2210 self.statsd.join()
2211 self.webapp.stop()
2212 self.webapp.join()
2213 self.rpc.stop()
2214 self.rpc.join()
2215 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002216 self.fake_nodepool.stop()
2217 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002218 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002219 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002220 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002221 # Further the pydevd threads also need to be whitelisted so debugging
2222 # e.g. in PyCharm is possible without breaking shutdown.
2223 whitelist = ['executor-watchdog',
2224 'pydevd.CommandThread',
2225 'pydevd.Reader',
2226 'pydevd.Writer',
2227 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002228 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002229 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002230 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002231 log_str = ""
2232 for thread_id, stack_frame in sys._current_frames().items():
2233 log_str += "Thread: %s\n" % thread_id
2234 log_str += "".join(traceback.format_stack(stack_frame))
2235 self.log.debug(log_str)
2236 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002237
James E. Blaira002b032017-04-18 10:35:48 -07002238 def assertCleanShutdown(self):
2239 pass
2240
James E. Blairc4ba97a2017-04-19 16:26:24 -07002241 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002242 parts = project.split('/')
2243 path = os.path.join(self.upstream_root, *parts[:-1])
2244 if not os.path.exists(path):
2245 os.makedirs(path)
2246 path = os.path.join(self.upstream_root, project)
2247 repo = git.Repo.init(path)
2248
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002249 with repo.config_writer() as config_writer:
2250 config_writer.set_value('user', 'email', 'user@example.com')
2251 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002252
Clark Boylanb640e052014-04-03 16:41:46 -07002253 repo.index.commit('initial commit')
2254 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002255 if tag:
2256 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002257
James E. Blair97d902e2014-08-21 13:25:56 -07002258 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002259 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002260 repo.git.clean('-x', '-f', '-d')
2261
James E. Blair97d902e2014-08-21 13:25:56 -07002262 def create_branch(self, project, branch):
2263 path = os.path.join(self.upstream_root, project)
2264 repo = git.Repo.init(path)
2265 fn = os.path.join(path, 'README')
2266
2267 branch_head = repo.create_head(branch)
2268 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002269 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002270 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002271 f.close()
2272 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002273 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002274
James E. Blair97d902e2014-08-21 13:25:56 -07002275 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002276 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002277 repo.git.clean('-x', '-f', '-d')
2278
Sachi King9f16d522016-03-16 12:20:45 +11002279 def create_commit(self, project):
2280 path = os.path.join(self.upstream_root, project)
2281 repo = git.Repo(path)
2282 repo.head.reference = repo.heads['master']
2283 file_name = os.path.join(path, 'README')
2284 with open(file_name, 'a') as f:
2285 f.write('creating fake commit\n')
2286 repo.index.add([file_name])
2287 commit = repo.index.commit('Creating a fake commit')
2288 return commit.hexsha
2289
James E. Blairf4a5f022017-04-18 14:01:10 -07002290 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002291 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002292 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002293 while len(self.builds):
2294 self.release(self.builds[0])
2295 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002296 i += 1
2297 if count is not None and i >= count:
2298 break
James E. Blairb8c16472015-05-05 14:55:26 -07002299
Clark Boylanb640e052014-04-03 16:41:46 -07002300 def release(self, job):
2301 if isinstance(job, FakeBuild):
2302 job.release()
2303 else:
2304 job.waiting = False
2305 self.log.debug("Queued job %s released" % job.unique)
2306 self.gearman_server.wakeConnections()
2307
2308 def getParameter(self, job, name):
2309 if isinstance(job, FakeBuild):
2310 return job.parameters[name]
2311 else:
2312 parameters = json.loads(job.arguments)
2313 return parameters[name]
2314
Clark Boylanb640e052014-04-03 16:41:46 -07002315 def haveAllBuildsReported(self):
2316 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002317 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002318 return False
2319 # Find out if every build that the worker has completed has been
2320 # reported back to Zuul. If it hasn't then that means a Gearman
2321 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002322 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002323 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002324 if not zbuild:
2325 # It has already been reported
2326 continue
2327 # It hasn't been reported yet.
2328 return False
2329 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002330 worker = self.executor_server.executor_worker
2331 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002332 if connection.state == 'GRAB_WAIT':
2333 return False
2334 return True
2335
2336 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002337 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002338 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002339 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002340 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002341 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002342 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002343 for j in conn.related_jobs.values():
2344 if j.unique == build.uuid:
2345 client_job = j
2346 break
2347 if not client_job:
2348 self.log.debug("%s is not known to the gearman client" %
2349 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002351 if not client_job.handle:
2352 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002353 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002354 server_job = self.gearman_server.jobs.get(client_job.handle)
2355 if not server_job:
2356 self.log.debug("%s is not known to the gearman server" %
2357 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002358 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002359 if not hasattr(server_job, 'waiting'):
2360 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002361 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002362 if server_job.waiting:
2363 continue
James E. Blair17302972016-08-10 16:11:42 -07002364 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002365 self.log.debug("%s has not reported start" % build)
2366 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002367 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002368 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002369 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002370 if worker_build:
2371 if worker_build.isWaiting():
2372 continue
2373 else:
2374 self.log.debug("%s is running" % worker_build)
2375 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002376 else:
James E. Blair962220f2016-08-03 11:22:38 -07002377 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002378 return False
James E. Blaira002b032017-04-18 10:35:48 -07002379 for (build_uuid, job_worker) in \
2380 self.executor_server.job_workers.items():
2381 if build_uuid not in seen_builds:
2382 self.log.debug("%s is not finalized" % build_uuid)
2383 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002384 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002385
James E. Blairdce6cea2016-12-20 16:45:32 -08002386 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002387 if self.fake_nodepool.paused:
2388 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002389 if self.sched.nodepool.requests:
2390 return False
2391 return True
2392
Jan Hruban6b71aff2015-10-22 16:58:08 +02002393 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002394 for event_queue in self.event_queues:
2395 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002396
2397 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002398 for event_queue in self.event_queues:
2399 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002400
Clark Boylanb640e052014-04-03 16:41:46 -07002401 def waitUntilSettled(self):
2402 self.log.debug("Waiting until settled...")
2403 start = time.time()
2404 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002405 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002406 self.log.error("Timeout waiting for Zuul to settle")
2407 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002408 for event_queue in self.event_queues:
2409 self.log.error(" %s: %s" %
2410 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002411 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002412 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002413 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002414 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002415 self.log.error("All requests completed: %s" %
2416 (self.areAllNodeRequestsComplete(),))
2417 self.log.error("Merge client jobs: %s" %
2418 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002419 raise Exception("Timeout waiting for Zuul to settle")
2420 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002421
Paul Belanger174a8272017-03-14 13:20:10 -04002422 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002423 # have all build states propogated to zuul?
2424 if self.haveAllBuildsReported():
2425 # Join ensures that the queue is empty _and_ events have been
2426 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002427 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002428 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002429 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002430 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002431 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002432 self.areAllNodeRequestsComplete() and
2433 all(self.eventQueuesEmpty())):
2434 # The queue empty check is placed at the end to
2435 # ensure that if a component adds an event between
2436 # when locked the run handler and checked that the
2437 # components were stable, we don't erroneously
2438 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002439 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002440 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002441 self.log.debug("...settled.")
2442 return
2443 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002444 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002445 self.sched.wake_event.wait(0.1)
2446
2447 def countJobResults(self, jobs, result):
2448 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002449 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002450
Monty Taylor0d926122017-05-24 08:07:56 -05002451 def getBuildByName(self, name):
2452 for build in self.builds:
2453 if build.name == name:
2454 return build
2455 raise Exception("Unable to find build %s" % name)
2456
James E. Blair96c6bf82016-01-15 16:20:40 -08002457 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002458 for job in self.history:
2459 if (job.name == name and
2460 (project is None or
2461 job.parameters['ZUUL_PROJECT'] == project)):
2462 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002463 raise Exception("Unable to find job %s in history" % name)
2464
2465 def assertEmptyQueues(self):
2466 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002467 for tenant in self.sched.abide.tenants.values():
2468 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002469 for pipeline_queue in pipeline.queues:
2470 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002471 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002472 pipeline.name, pipeline_queue.name,
2473 pipeline_queue.queue))
2474 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002475 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002476
2477 def assertReportedStat(self, key, value=None, kind=None):
2478 start = time.time()
2479 while time.time() < (start + 5):
2480 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002481 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002482 if key == k:
2483 if value is None and kind is None:
2484 return
2485 elif value:
2486 if value == v:
2487 return
2488 elif kind:
2489 if v.endswith('|' + kind):
2490 return
2491 time.sleep(0.1)
2492
Clark Boylanb640e052014-04-03 16:41:46 -07002493 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002494
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002495 def assertBuilds(self, builds):
2496 """Assert that the running builds are as described.
2497
2498 The list of running builds is examined and must match exactly
2499 the list of builds described by the input.
2500
2501 :arg list builds: A list of dictionaries. Each item in the
2502 list must match the corresponding build in the build
2503 history, and each element of the dictionary must match the
2504 corresponding attribute of the build.
2505
2506 """
James E. Blair3158e282016-08-19 09:34:11 -07002507 try:
2508 self.assertEqual(len(self.builds), len(builds))
2509 for i, d in enumerate(builds):
2510 for k, v in d.items():
2511 self.assertEqual(
2512 getattr(self.builds[i], k), v,
2513 "Element %i in builds does not match" % (i,))
2514 except Exception:
2515 for build in self.builds:
2516 self.log.error("Running build: %s" % build)
2517 else:
2518 self.log.error("No running builds")
2519 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002520
James E. Blairb536ecc2016-08-31 10:11:42 -07002521 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002522 """Assert that the completed builds are as described.
2523
2524 The list of completed builds is examined and must match
2525 exactly the list of builds described by the input.
2526
2527 :arg list history: A list of dictionaries. Each item in the
2528 list must match the corresponding build in the build
2529 history, and each element of the dictionary must match the
2530 corresponding attribute of the build.
2531
James E. Blairb536ecc2016-08-31 10:11:42 -07002532 :arg bool ordered: If true, the history must match the order
2533 supplied, if false, the builds are permitted to have
2534 arrived in any order.
2535
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002536 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002537 def matches(history_item, item):
2538 for k, v in item.items():
2539 if getattr(history_item, k) != v:
2540 return False
2541 return True
James E. Blair3158e282016-08-19 09:34:11 -07002542 try:
2543 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002544 if ordered:
2545 for i, d in enumerate(history):
2546 if not matches(self.history[i], d):
2547 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002548 "Element %i in history does not match %s" %
2549 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002550 else:
2551 unseen = self.history[:]
2552 for i, d in enumerate(history):
2553 found = False
2554 for unseen_item in unseen:
2555 if matches(unseen_item, d):
2556 found = True
2557 unseen.remove(unseen_item)
2558 break
2559 if not found:
2560 raise Exception("No match found for element %i "
2561 "in history" % (i,))
2562 if unseen:
2563 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002564 except Exception:
2565 for build in self.history:
2566 self.log.error("Completed build: %s" % build)
2567 else:
2568 self.log.error("No completed builds")
2569 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002570
James E. Blair6ac368c2016-12-22 18:07:20 -08002571 def printHistory(self):
2572 """Log the build history.
2573
2574 This can be useful during tests to summarize what jobs have
2575 completed.
2576
2577 """
2578 self.log.debug("Build history:")
2579 for build in self.history:
2580 self.log.debug(build)
2581
James E. Blair59fdbac2015-12-07 17:08:06 -08002582 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002583 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2584
James E. Blair9ea70072017-04-19 16:05:30 -07002585 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002586 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002587 if not os.path.exists(root):
2588 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002589 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2590 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002591- tenant:
2592 name: openstack
2593 source:
2594 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002595 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002596 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002597 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002598 - org/project
2599 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002600 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002601 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002602 self.config.set('zuul', 'tenant_config',
2603 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002604 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002605
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002606 def addCommitToRepo(self, project, message, files,
2607 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002608 path = os.path.join(self.upstream_root, project)
2609 repo = git.Repo(path)
2610 repo.head.reference = branch
2611 zuul.merger.merger.reset_repo_to_head(repo)
2612 for fn, content in files.items():
2613 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002614 try:
2615 os.makedirs(os.path.dirname(fn))
2616 except OSError:
2617 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002618 with open(fn, 'w') as f:
2619 f.write(content)
2620 repo.index.add([fn])
2621 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002622 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002623 repo.heads[branch].commit = commit
2624 repo.head.reference = branch
2625 repo.git.clean('-x', '-f', '-d')
2626 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002627 if tag:
2628 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002629 return before
2630
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002631 def commitConfigUpdate(self, project_name, source_name):
2632 """Commit an update to zuul.yaml
2633
2634 This overwrites the zuul.yaml in the specificed project with
2635 the contents specified.
2636
2637 :arg str project_name: The name of the project containing
2638 zuul.yaml (e.g., common-config)
2639
2640 :arg str source_name: The path to the file (underneath the
2641 test fixture directory) whose contents should be used to
2642 replace zuul.yaml.
2643 """
2644
2645 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002646 files = {}
2647 with open(source_path, 'r') as f:
2648 data = f.read()
2649 layout = yaml.safe_load(data)
2650 files['zuul.yaml'] = data
2651 for item in layout:
2652 if 'job' in item:
2653 jobname = item['job']['name']
2654 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002655 before = self.addCommitToRepo(
2656 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002657 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002658 return before
2659
James E. Blair7fc8daa2016-08-08 15:37:15 -07002660 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002661
James E. Blair7fc8daa2016-08-08 15:37:15 -07002662 """Inject a Fake (Gerrit) event.
2663
2664 This method accepts a JSON-encoded event and simulates Zuul
2665 having received it from Gerrit. It could (and should)
2666 eventually apply to any connection type, but is currently only
2667 used with Gerrit connections. The name of the connection is
2668 used to look up the corresponding server, and the event is
2669 simulated as having been received by all Zuul connections
2670 attached to that server. So if two Gerrit connections in Zuul
2671 are connected to the same Gerrit server, and you invoke this
2672 method specifying the name of one of them, the event will be
2673 received by both.
2674
2675 .. note::
2676
2677 "self.fake_gerrit.addEvent" calls should be migrated to
2678 this method.
2679
2680 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002681 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002682 :arg str event: The JSON-encoded event.
2683
2684 """
2685 specified_conn = self.connections.connections[connection]
2686 for conn in self.connections.connections.values():
2687 if (isinstance(conn, specified_conn.__class__) and
2688 specified_conn.server == conn.server):
2689 conn.addEvent(event)
2690
James E. Blaird8af5422017-05-24 13:59:40 -07002691 def getUpstreamRepos(self, projects):
2692 """Return upstream git repo objects for the listed projects
2693
2694 :arg list projects: A list of strings, each the canonical name
2695 of a project.
2696
2697 :returns: A dictionary of {name: repo} for every listed
2698 project.
2699 :rtype: dict
2700
2701 """
2702
2703 repos = {}
2704 for project in projects:
2705 # FIXME(jeblair): the upstream root does not yet have a
2706 # hostname component; that needs to be added, and this
2707 # line removed:
2708 tmp_project_name = '/'.join(project.split('/')[1:])
2709 path = os.path.join(self.upstream_root, tmp_project_name)
2710 repo = git.Repo(path)
2711 repos[project] = repo
2712 return repos
2713
James E. Blair3f876d52016-07-22 13:07:14 -07002714
2715class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002716 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002717 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002718
Joshua Heskethd78b4482015-09-14 16:56:34 -06002719
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002720class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002721 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002722 use_ssl = True
2723
2724
Joshua Heskethd78b4482015-09-14 16:56:34 -06002725class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002726 def setup_config(self):
2727 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002728 for section_name in self.config.sections():
2729 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2730 section_name, re.I)
2731 if not con_match:
2732 continue
2733
2734 if self.config.get(section_name, 'driver') == 'sql':
2735 f = MySQLSchemaFixture()
2736 self.useFixture(f)
2737 if (self.config.get(section_name, 'dburi') ==
2738 '$MYSQL_FIXTURE_DBURI$'):
2739 self.config.set(section_name, 'dburi', f.dburi)