blob: fae23fef2ec8dca644ab95d753cd71b91403e40e [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
22import json
23import logging
24import os
Christian Berendt12d4d722014-06-07 21:03:45 +020025from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070026from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070027import random
28import re
29import select
30import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030031from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070032try:
33 from cStringIO import StringIO
34except Exception:
35 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070036import socket
37import string
38import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080039import sys
James E. Blairf84026c2015-12-08 16:11:46 -080040import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070041import threading
Clark Boylan8208c192017-04-24 18:08:08 -070042import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070043import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060044import uuid
45
Clark Boylanb640e052014-04-03 16:41:46 -070046
47import git
48import gear
49import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080050import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080051import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060052import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070053import statsd
54import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080055import testtools.content
56import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080057from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000058import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070059
James E. Blaire511d2f2016-12-08 15:22:26 -080060import zuul.driver.gerrit.gerritsource as gerritsource
61import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070062import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.scheduler
64import zuul.webapp
65import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040066import zuul.executor.server
67import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080068import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070069import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070070import zuul.merger.merger
71import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020072import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070073import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080074import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010075from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070076
77FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
78 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080079
80KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070081
Clark Boylanb640e052014-04-03 16:41:46 -070082
83def repack_repo(path):
84 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
85 output = subprocess.Popen(cmd, close_fds=True,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.PIPE)
88 out = output.communicate()
89 if output.returncode:
90 raise Exception("git repack returned %d" % output.returncode)
91 return out
92
93
94def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040095 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070096
97
James E. Blaira190f3b2015-01-05 14:56:54 -080098def iterate_timeout(max_seconds, purpose):
99 start = time.time()
100 count = 0
101 while (time.time() < start + max_seconds):
102 count += 1
103 yield count
104 time.sleep(0)
105 raise Exception("Timeout waiting for %s" % purpose)
106
107
Jesse Keating436a5452017-04-20 11:48:41 -0700108def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700109 """Specify a layout file for use by a test method.
110
111 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700112 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700113
114 Some tests require only a very simple configuration. For those,
115 establishing a complete config directory hierachy is too much
116 work. In those cases, you can add a simple zuul.yaml file to the
117 test fixtures directory (in fixtures/layouts/foo.yaml) and use
118 this decorator to indicate the test method should use that rather
119 than the tenant config file specified by the test class.
120
121 The decorator will cause that layout file to be added to a
122 config-project called "common-config" and each "project" instance
123 referenced in the layout file will have a git repo automatically
124 initialized.
125 """
126
127 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700128 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700129 return test
130 return decorator
131
132
Gregory Haynes4fc12542015-04-22 20:38:06 -0700133class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700134 _common_path_default = "refs/changes"
135 _points_to_commits_only = True
136
137
Gregory Haynes4fc12542015-04-22 20:38:06 -0700138class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700139 categories = {'approved': ('Approved', -1, 1),
140 'code-review': ('Code-Review', -2, 2),
141 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700142
143 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700144 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700146 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700147 self.reported = 0
148 self.queried = 0
149 self.patchsets = []
150 self.number = number
151 self.project = project
152 self.branch = branch
153 self.subject = subject
154 self.latest_patchset = 0
155 self.depends_on_change = None
156 self.needed_by_changes = []
157 self.fail_merge = False
158 self.messages = []
159 self.data = {
160 'branch': branch,
161 'comments': [],
162 'commitMessage': subject,
163 'createdOn': time.time(),
164 'id': 'I' + random_sha1(),
165 'lastUpdated': time.time(),
166 'number': str(number),
167 'open': status == 'NEW',
168 'owner': {'email': 'user@example.com',
169 'name': 'User Name',
170 'username': 'username'},
171 'patchSets': self.patchsets,
172 'project': project,
173 'status': status,
174 'subject': subject,
175 'submitRecords': [],
176 'url': 'https://hostname/%s' % number}
177
178 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700179 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700180 self.data['submitRecords'] = self.getSubmitRecords()
181 self.open = status == 'NEW'
182
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700183 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700184 path = os.path.join(self.upstream_root, self.project)
185 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
188 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
197 with open(fn, 'w') as f:
198 f.write(content)
199 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700200 else:
201 for fni in range(100):
202 fn = os.path.join(path, str(fni))
203 f = open(fn, 'w')
204 for ci in range(4096):
205 f.write(random.choice(string.printable))
206 f.close()
207 repo.index.add([fn])
208
209 r = repo.index.commit(msg)
210 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700211 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700212 repo.git.clean('-x', '-f', '-d')
213 repo.heads['master'].checkout()
214 return r
215
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700217 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700219 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700220 data = ("test %s %s %s\n" %
221 (self.branch, self.number, self.latest_patchset))
222 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700223 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700225 ps_files = [{'file': '/COMMIT_MSG',
226 'type': 'ADDED'},
227 {'file': 'README',
228 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700229 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700230 ps_files.append({'file': f, 'type': 'ADDED'})
231 d = {'approvals': [],
232 'createdOn': time.time(),
233 'files': ps_files,
234 'number': str(self.latest_patchset),
235 'ref': 'refs/changes/1/%s/%s' % (self.number,
236 self.latest_patchset),
237 'revision': c.hexsha,
238 'uploader': {'email': 'user@example.com',
239 'name': 'User name',
240 'username': 'user'}}
241 self.data['currentPatchSet'] = d
242 self.patchsets.append(d)
243 self.data['submitRecords'] = self.getSubmitRecords()
244
245 def getPatchsetCreatedEvent(self, patchset):
246 event = {"type": "patchset-created",
247 "change": {"project": self.project,
248 "branch": self.branch,
249 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
250 "number": str(self.number),
251 "subject": self.subject,
252 "owner": {"name": "User Name"},
253 "url": "https://hostname/3"},
254 "patchSet": self.patchsets[patchset - 1],
255 "uploader": {"name": "User Name"}}
256 return event
257
258 def getChangeRestoredEvent(self):
259 event = {"type": "change-restored",
260 "change": {"project": self.project,
261 "branch": self.branch,
262 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
263 "number": str(self.number),
264 "subject": self.subject,
265 "owner": {"name": "User Name"},
266 "url": "https://hostname/3"},
267 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100268 "patchSet": self.patchsets[-1],
269 "reason": ""}
270 return event
271
272 def getChangeAbandonedEvent(self):
273 event = {"type": "change-abandoned",
274 "change": {"project": self.project,
275 "branch": self.branch,
276 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
277 "number": str(self.number),
278 "subject": self.subject,
279 "owner": {"name": "User Name"},
280 "url": "https://hostname/3"},
281 "abandoner": {"name": "User Name"},
282 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700283 "reason": ""}
284 return event
285
286 def getChangeCommentEvent(self, patchset):
287 event = {"type": "comment-added",
288 "change": {"project": self.project,
289 "branch": self.branch,
290 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
291 "number": str(self.number),
292 "subject": self.subject,
293 "owner": {"name": "User Name"},
294 "url": "https://hostname/3"},
295 "patchSet": self.patchsets[patchset - 1],
296 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700297 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700298 "description": "Code-Review",
299 "value": "0"}],
300 "comment": "This is a comment"}
301 return event
302
James E. Blairc2a5ed72017-02-20 14:12:01 -0500303 def getChangeMergedEvent(self):
304 event = {"submitter": {"name": "Jenkins",
305 "username": "jenkins"},
306 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
307 "patchSet": self.patchsets[-1],
308 "change": self.data,
309 "type": "change-merged",
310 "eventCreatedOn": 1487613810}
311 return event
312
James E. Blair8cce42e2016-10-18 08:18:36 -0700313 def getRefUpdatedEvent(self):
314 path = os.path.join(self.upstream_root, self.project)
315 repo = git.Repo(path)
316 oldrev = repo.heads[self.branch].commit.hexsha
317
318 event = {
319 "type": "ref-updated",
320 "submitter": {
321 "name": "User Name",
322 },
323 "refUpdate": {
324 "oldRev": oldrev,
325 "newRev": self.patchsets[-1]['revision'],
326 "refName": self.branch,
327 "project": self.project,
328 }
329 }
330 return event
331
Joshua Hesketh642824b2014-07-01 17:54:59 +1000332 def addApproval(self, category, value, username='reviewer_john',
333 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700334 if not granted_on:
335 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000336 approval = {
337 'description': self.categories[category][0],
338 'type': category,
339 'value': str(value),
340 'by': {
341 'username': username,
342 'email': username + '@example.com',
343 },
344 'grantedOn': int(granted_on)
345 }
Clark Boylanb640e052014-04-03 16:41:46 -0700346 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
347 if x['by']['username'] == username and x['type'] == category:
348 del self.patchsets[-1]['approvals'][i]
349 self.patchsets[-1]['approvals'].append(approval)
350 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000351 'author': {'email': 'author@example.com',
352 'name': 'Patchset Author',
353 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700354 'change': {'branch': self.branch,
355 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
356 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'owner': {'email': 'owner@example.com',
358 'name': 'Change Owner',
359 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'project': self.project,
361 'subject': self.subject,
362 'topic': 'master',
363 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700365 'patchSet': self.patchsets[-1],
366 'type': 'comment-added'}
367 self.data['submitRecords'] = self.getSubmitRecords()
368 return json.loads(json.dumps(event))
369
370 def getSubmitRecords(self):
371 status = {}
372 for cat in self.categories.keys():
373 status[cat] = 0
374
375 for a in self.patchsets[-1]['approvals']:
376 cur = status[a['type']]
377 cat_min, cat_max = self.categories[a['type']][1:]
378 new = int(a['value'])
379 if new == cat_min:
380 cur = new
381 elif abs(new) > abs(cur):
382 cur = new
383 status[a['type']] = cur
384
385 labels = []
386 ok = True
387 for typ, cat in self.categories.items():
388 cur = status[typ]
389 cat_min, cat_max = cat[1:]
390 if cur == cat_min:
391 value = 'REJECT'
392 ok = False
393 elif cur == cat_max:
394 value = 'OK'
395 else:
396 value = 'NEED'
397 ok = False
398 labels.append({'label': cat[0], 'status': value})
399 if ok:
400 return [{'status': 'OK'}]
401 return [{'status': 'NOT_READY',
402 'labels': labels}]
403
404 def setDependsOn(self, other, patchset):
405 self.depends_on_change = other
406 d = {'id': other.data['id'],
407 'number': other.data['number'],
408 'ref': other.patchsets[patchset - 1]['ref']
409 }
410 self.data['dependsOn'] = [d]
411
412 other.needed_by_changes.append(self)
413 needed = other.data.get('neededBy', [])
414 d = {'id': self.data['id'],
415 'number': self.data['number'],
416 'ref': self.patchsets[patchset - 1]['ref'],
417 'revision': self.patchsets[patchset - 1]['revision']
418 }
419 needed.append(d)
420 other.data['neededBy'] = needed
421
422 def query(self):
423 self.queried += 1
424 d = self.data.get('dependsOn')
425 if d:
426 d = d[0]
427 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
428 d['isCurrentPatchSet'] = True
429 else:
430 d['isCurrentPatchSet'] = False
431 return json.loads(json.dumps(self.data))
432
433 def setMerged(self):
434 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000435 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700436 return
437 if self.fail_merge:
438 return
439 self.data['status'] = 'MERGED'
440 self.open = False
441
442 path = os.path.join(self.upstream_root, self.project)
443 repo = git.Repo(path)
444 repo.heads[self.branch].commit = \
445 repo.commit(self.patchsets[-1]['revision'])
446
447 def setReported(self):
448 self.reported += 1
449
450
James E. Blaire511d2f2016-12-08 15:22:26 -0800451class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700452 """A Fake Gerrit connection for use in tests.
453
454 This subclasses
455 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
456 ability for tests to add changes to the fake Gerrit it represents.
457 """
458
Joshua Hesketh352264b2015-08-11 23:42:08 +1000459 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700460
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700462 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800463 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 connection_config)
465
James E. Blair7fc8daa2016-08-08 15:37:15 -0700466 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700467 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
468 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700470 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200471 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700472
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700473 def addFakeChange(self, project, branch, subject, status='NEW',
474 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700475 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700476 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700477 c = FakeGerritChange(self, self.change_number, project, branch,
478 subject, upstream_root=self.upstream_root,
479 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700480 self.changes[self.change_number] = c
481 return c
482
Clark Boylanb640e052014-04-03 16:41:46 -0700483 def review(self, project, changeid, message, action):
484 number, ps = changeid.split(',')
485 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000486
487 # Add the approval back onto the change (ie simulate what gerrit would
488 # do).
489 # Usually when zuul leaves a review it'll create a feedback loop where
490 # zuul's review enters another gerrit event (which is then picked up by
491 # zuul). However, we can't mimic this behaviour (by adding this
492 # approval event into the queue) as it stops jobs from checking what
493 # happens before this event is triggered. If a job needs to see what
494 # happens they can add their own verified event into the queue.
495 # Nevertheless, we can update change with the new review in gerrit.
496
James E. Blair8b5408c2016-08-08 15:37:46 -0700497 for cat in action.keys():
498 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000499 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000502
Clark Boylanb640e052014-04-03 16:41:46 -0700503 if 'submit' in action:
504 change.setMerged()
505 if message:
506 change.setReported()
507
508 def query(self, number):
509 change = self.changes.get(int(number))
510 if change:
511 return change.query()
512 return {}
513
James E. Blairc494d542014-08-06 09:23:52 -0700514 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700515 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700516 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800517 if query.startswith('change:'):
518 # Query a specific changeid
519 changeid = query[len('change:'):]
520 l = [change.query() for change in self.changes.values()
521 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700522 elif query.startswith('message:'):
523 # Query the content of a commit message
524 msg = query[len('message:'):].strip()
525 l = [change.query() for change in self.changes.values()
526 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800527 else:
528 # Query all open changes
529 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700530 return l
James E. Blairc494d542014-08-06 09:23:52 -0700531
Joshua Hesketh352264b2015-08-11 23:42:08 +1000532 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700533 pass
534
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200535 def _uploadPack(self, project):
536 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
537 'multi_ack thin-pack side-band side-band-64k ofs-delta '
538 'shallow no-progress include-tag multi_ack_detailed no-done\n')
539 path = os.path.join(self.upstream_root, project.name)
540 repo = git.Repo(path)
541 for ref in repo.refs:
542 r = ref.object.hexsha + ' ' + ref.path + '\n'
543 ret += '%04x%s' % (len(r) + 4, r)
544 ret += '0000'
545 return ret
546
Joshua Hesketh352264b2015-08-11 23:42:08 +1000547 def getGitUrl(self, project):
548 return os.path.join(self.upstream_root, project.name)
549
Clark Boylanb640e052014-04-03 16:41:46 -0700550
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551class GithubChangeReference(git.Reference):
552 _common_path_default = "refs/pull"
553 _points_to_commits_only = True
554
555
556class FakeGithubPullRequest(object):
557
558 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800559 subject, upstream_root, files=[], number_of_commits=1,
560 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700561 """Creates a new PR with several commits.
562 Sends an event about opened PR."""
563 self.github = github
564 self.source = github
565 self.number = number
566 self.project = project
567 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100568 self.subject = subject
569 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100571 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700572 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100573 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100574 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800575 self.reviews = []
576 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700577 self.updated_at = None
578 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100579 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100580 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700581 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 self._updateTimeStamp()
585
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 self._updateTimeStamp()
590
Jan Hruban570d01c2016-03-10 21:51:32 +0100591 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100593 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700594 self._updateTimeStamp()
595
596 def getPullRequestOpenedEvent(self):
597 return self._getPullRequestEvent('opened')
598
599 def getPullRequestSynchronizeEvent(self):
600 return self._getPullRequestEvent('synchronize')
601
602 def getPullRequestReopenedEvent(self):
603 return self._getPullRequestEvent('reopened')
604
605 def getPullRequestClosedEvent(self):
606 return self._getPullRequestEvent('closed')
607
608 def addComment(self, message):
609 self.comments.append(message)
610 self._updateTimeStamp()
611
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200612 def getCommentAddedEvent(self, text):
613 name = 'issue_comment'
614 data = {
615 'action': 'created',
616 'issue': {
617 'number': self.number
618 },
619 'comment': {
620 'body': text
621 },
622 'repository': {
623 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100624 },
625 'sender': {
626 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200627 }
628 }
629 return (name, data)
630
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800631 def getReviewAddedEvent(self, review):
632 name = 'pull_request_review'
633 data = {
634 'action': 'submitted',
635 'pull_request': {
636 'number': self.number,
637 'title': self.subject,
638 'updated_at': self.updated_at,
639 'base': {
640 'ref': self.branch,
641 'repo': {
642 'full_name': self.project
643 }
644 },
645 'head': {
646 'sha': self.head_sha
647 }
648 },
649 'review': {
650 'state': review
651 },
652 'repository': {
653 'full_name': self.project
654 },
655 'sender': {
656 'login': 'ghuser'
657 }
658 }
659 return (name, data)
660
Jan Hruban16ad31f2015-11-07 14:39:07 +0100661 def addLabel(self, name):
662 if name not in self.labels:
663 self.labels.append(name)
664 self._updateTimeStamp()
665 return self._getLabelEvent(name)
666
667 def removeLabel(self, name):
668 if name in self.labels:
669 self.labels.remove(name)
670 self._updateTimeStamp()
671 return self._getUnlabelEvent(name)
672
673 def _getLabelEvent(self, label):
674 name = 'pull_request'
675 data = {
676 'action': 'labeled',
677 'pull_request': {
678 'number': self.number,
679 'updated_at': self.updated_at,
680 'base': {
681 'ref': self.branch,
682 'repo': {
683 'full_name': self.project
684 }
685 },
686 'head': {
687 'sha': self.head_sha
688 }
689 },
690 'label': {
691 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100692 },
693 'sender': {
694 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 }
696 }
697 return (name, data)
698
699 def _getUnlabelEvent(self, label):
700 name = 'pull_request'
701 data = {
702 'action': 'unlabeled',
703 'pull_request': {
704 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100705 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100706 'updated_at': self.updated_at,
707 'base': {
708 'ref': self.branch,
709 'repo': {
710 'full_name': self.project
711 }
712 },
713 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800714 'sha': self.head_sha,
715 'repo': {
716 'full_name': self.project
717 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100718 }
719 },
720 'label': {
721 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100722 },
723 'sender': {
724 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100725 }
726 }
727 return (name, data)
728
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 def _getRepo(self):
730 repo_path = os.path.join(self.upstream_root, self.project)
731 return git.Repo(repo_path)
732
733 def _createPRRef(self):
734 repo = self._getRepo()
735 GithubChangeReference.create(
736 repo, self._getPRReference(), 'refs/tags/init')
737
Jan Hruban570d01c2016-03-10 21:51:32 +0100738 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700739 repo = self._getRepo()
740 ref = repo.references[self._getPRReference()]
741 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100742 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700743 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100744 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700745 repo.head.reference = ref
746 zuul.merger.merger.reset_repo_to_head(repo)
747 repo.git.clean('-x', '-f', '-d')
748
Jan Hruban570d01c2016-03-10 21:51:32 +0100749 if files:
750 fn = files[0]
751 self.files = files
752 else:
753 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
754 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100755 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700756 fn = os.path.join(repo.working_dir, fn)
757 f = open(fn, 'w')
758 with open(fn, 'w') as f:
759 f.write("test %s %s\n" %
760 (self.branch, self.number))
761 repo.index.add([fn])
762
763 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800764 # Create an empty set of statuses for the given sha,
765 # each sha on a PR may have a status set on it
766 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700767 repo.head.reference = 'master'
768 zuul.merger.merger.reset_repo_to_head(repo)
769 repo.git.clean('-x', '-f', '-d')
770 repo.heads['master'].checkout()
771
772 def _updateTimeStamp(self):
773 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
774
775 def getPRHeadSha(self):
776 repo = self._getRepo()
777 return repo.references[self._getPRReference()].commit.hexsha
778
Jesse Keatingae4cd272017-01-30 17:10:44 -0800779 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800780 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
781 # convert the timestamp to a str format that would be returned
782 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800783
Adam Gandelmand81dd762017-02-09 15:15:49 -0800784 if granted_on:
785 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
786 submitted_at = time.strftime(
787 gh_time_format, granted_on.timetuple())
788 else:
789 # github timestamps only down to the second, so we need to make
790 # sure reviews that tests add appear to be added over a period of
791 # time in the past and not all at once.
792 if not self.reviews:
793 # the first review happens 10 mins ago
794 offset = 600
795 else:
796 # subsequent reviews happen 1 minute closer to now
797 offset = 600 - (len(self.reviews) * 60)
798
799 granted_on = datetime.datetime.utcfromtimestamp(
800 time.time() - offset)
801 submitted_at = time.strftime(
802 gh_time_format, granted_on.timetuple())
803
Jesse Keatingae4cd272017-01-30 17:10:44 -0800804 self.reviews.append({
805 'state': state,
806 'user': {
807 'login': user,
808 'email': user + "@derp.com",
809 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800810 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800811 })
812
Gregory Haynes4fc12542015-04-22 20:38:06 -0700813 def _getPRReference(self):
814 return '%s/head' % self.number
815
816 def _getPullRequestEvent(self, action):
817 name = 'pull_request'
818 data = {
819 'action': action,
820 'number': self.number,
821 'pull_request': {
822 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100823 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700824 'updated_at': self.updated_at,
825 'base': {
826 'ref': self.branch,
827 'repo': {
828 'full_name': self.project
829 }
830 },
831 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800832 'sha': self.head_sha,
833 'repo': {
834 'full_name': self.project
835 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700836 }
Jan Hruban3b415922016-02-03 13:10:22 +0100837 },
838 'sender': {
839 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700840 }
841 }
842 return (name, data)
843
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800844 def getCommitStatusEvent(self, context, state='success', user='zuul'):
845 name = 'status'
846 data = {
847 'state': state,
848 'sha': self.head_sha,
849 'description': 'Test results for %s: %s' % (self.head_sha, state),
850 'target_url': 'http://zuul/%s' % self.head_sha,
851 'branches': [],
852 'context': context,
853 'sender': {
854 'login': user
855 }
856 }
857 return (name, data)
858
Gregory Haynes4fc12542015-04-22 20:38:06 -0700859
860class FakeGithubConnection(githubconnection.GithubConnection):
861 log = logging.getLogger("zuul.test.FakeGithubConnection")
862
863 def __init__(self, driver, connection_name, connection_config,
864 upstream_root=None):
865 super(FakeGithubConnection, self).__init__(driver, connection_name,
866 connection_config)
867 self.connection_name = connection_name
868 self.pr_number = 0
869 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700870 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700871 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100872 self.merge_failure = False
873 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874
Jan Hruban570d01c2016-03-10 21:51:32 +0100875 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700876 self.pr_number += 1
877 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100878 self, self.pr_number, project, branch, subject, self.upstream_root,
879 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880 self.pull_requests.append(pull_request)
881 return pull_request
882
Jesse Keating71a47ff2017-06-06 11:36:43 -0700883 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
884 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700885 if not old_rev:
886 old_rev = '00000000000000000000000000000000'
887 if not new_rev:
888 new_rev = random_sha1()
889 name = 'push'
890 data = {
891 'ref': ref,
892 'before': old_rev,
893 'after': new_rev,
894 'repository': {
895 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700896 },
897 'commits': [
898 {
899 'added': added_files,
900 'removed': removed_files,
901 'modified': modified_files
902 }
903 ]
Wayne1a78c612015-06-11 17:14:13 -0700904 }
905 return (name, data)
906
Gregory Haynes4fc12542015-04-22 20:38:06 -0700907 def emitEvent(self, event):
908 """Emulates sending the GitHub webhook event to the connection."""
909 port = self.webapp.server.socket.getsockname()[1]
910 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700911 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 headers = {'X-Github-Event': name}
913 req = urllib.request.Request(
914 'http://localhost:%s/connection/%s/payload'
915 % (port, self.connection_name),
916 data=payload, headers=headers)
917 urllib.request.urlopen(req)
918
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200919 def getPull(self, project, number):
920 pr = self.pull_requests[number - 1]
921 data = {
922 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100923 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200924 'updated_at': pr.updated_at,
925 'base': {
926 'repo': {
927 'full_name': pr.project
928 },
929 'ref': pr.branch,
930 },
Jan Hruban37615e52015-11-19 14:30:49 +0100931 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700932 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800934 'sha': pr.head_sha,
935 'repo': {
936 'full_name': pr.project
937 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 }
939 }
940 return data
941
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800942 def getPullBySha(self, sha):
943 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
944 if len(prs) > 1:
945 raise Exception('Multiple pulls found with head sha: %s' % sha)
946 pr = prs[0]
947 return self.getPull(pr.project, pr.number)
948
Jan Hruban570d01c2016-03-10 21:51:32 +0100949 def getPullFileNames(self, project, number):
950 pr = self.pull_requests[number - 1]
951 return pr.files
952
Jesse Keatingae4cd272017-01-30 17:10:44 -0800953 def _getPullReviews(self, owner, project, number):
954 pr = self.pull_requests[number - 1]
955 return pr.reviews
956
Jan Hruban3b415922016-02-03 13:10:22 +0100957 def getUser(self, login):
958 data = {
959 'username': login,
960 'name': 'Github User',
961 'email': 'github.user@example.com'
962 }
963 return data
964
Jesse Keatingae4cd272017-01-30 17:10:44 -0800965 def getRepoPermission(self, project, login):
966 owner, proj = project.split('/')
967 for pr in self.pull_requests:
968 pr_owner, pr_project = pr.project.split('/')
969 if (pr_owner == owner and proj == pr_project):
970 if login in pr.writers:
971 return 'write'
972 else:
973 return 'read'
974
Gregory Haynes4fc12542015-04-22 20:38:06 -0700975 def getGitUrl(self, project):
976 return os.path.join(self.upstream_root, str(project))
977
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200978 def real_getGitUrl(self, project):
979 return super(FakeGithubConnection, self).getGitUrl(project)
980
Gregory Haynes4fc12542015-04-22 20:38:06 -0700981 def getProjectBranches(self, project):
982 """Masks getProjectBranches since we don't have a real github"""
983
984 # just returns master for now
985 return ['master']
986
Jan Hrubane252a732017-01-03 15:03:09 +0100987 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700988 pull_request = self.pull_requests[pr_number - 1]
989 pull_request.addComment(message)
990
Jan Hruban3b415922016-02-03 13:10:22 +0100991 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100992 pull_request = self.pull_requests[pr_number - 1]
993 if self.merge_failure:
994 raise Exception('Pull request was not merged')
995 if self.merge_not_allowed_count > 0:
996 self.merge_not_allowed_count -= 1
997 raise MergeFailure('Merge was not successful due to mergeability'
998 ' conflict')
999 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001000 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001001
Jesse Keatingd96e5882017-01-19 13:55:50 -08001002 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001003 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001004
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001005 def setCommitStatus(self, project, sha, state, url='', description='',
1006 context='default', user='zuul'):
1007 # always insert a status to the front of the list, to represent
1008 # the last status provided for a commit.
1009 # Since we're bypassing github API, which would require a user, we
1010 # default the user as 'zuul' here.
1011 self.statuses.setdefault(project, {}).setdefault(sha, [])
1012 self.statuses[project][sha].insert(0, {
1013 'state': state,
1014 'url': url,
1015 'description': description,
1016 'context': context,
1017 'creator': {
1018 'login': user
1019 }
1020 })
Jan Hrubane252a732017-01-03 15:03:09 +01001021
Jan Hruban16ad31f2015-11-07 14:39:07 +01001022 def labelPull(self, project, pr_number, label):
1023 pull_request = self.pull_requests[pr_number - 1]
1024 pull_request.addLabel(label)
1025
1026 def unlabelPull(self, project, pr_number, label):
1027 pull_request = self.pull_requests[pr_number - 1]
1028 pull_request.removeLabel(label)
1029
Gregory Haynes4fc12542015-04-22 20:38:06 -07001030
Clark Boylanb640e052014-04-03 16:41:46 -07001031class BuildHistory(object):
1032 def __init__(self, **kw):
1033 self.__dict__.update(kw)
1034
1035 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001036 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1037 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001038
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040class FakeStatsd(threading.Thread):
1041 def __init__(self):
1042 threading.Thread.__init__(self)
1043 self.daemon = True
1044 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1045 self.sock.bind(('', 0))
1046 self.port = self.sock.getsockname()[1]
1047 self.wake_read, self.wake_write = os.pipe()
1048 self.stats = []
1049
1050 def run(self):
1051 while True:
1052 poll = select.poll()
1053 poll.register(self.sock, select.POLLIN)
1054 poll.register(self.wake_read, select.POLLIN)
1055 ret = poll.poll()
1056 for (fd, event) in ret:
1057 if fd == self.sock.fileno():
1058 data = self.sock.recvfrom(1024)
1059 if not data:
1060 return
1061 self.stats.append(data[0])
1062 if fd == self.wake_read:
1063 return
1064
1065 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001066 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001067
1068
James E. Blaire1767bc2016-08-02 10:00:27 -07001069class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001070 log = logging.getLogger("zuul.test")
1071
Paul Belanger174a8272017-03-14 13:20:10 -04001072 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001073 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001074 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001075 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001076 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001077 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001078 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001079 # TODOv3(jeblair): self.node is really "the label of the node
1080 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001081 # keep using it like this, or we may end up exposing more of
1082 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001083 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001084 self.node = None
1085 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001086 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001087 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001088 self.pipeline = self.parameters['ZUUL_PIPELINE']
1089 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001090 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001091 self.wait_condition = threading.Condition()
1092 self.waiting = False
1093 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001094 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001095 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001096 self.changes = None
1097 if 'ZUUL_CHANGE_IDS' in self.parameters:
1098 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001099
James E. Blair3158e282016-08-19 09:34:11 -07001100 def __repr__(self):
1101 waiting = ''
1102 if self.waiting:
1103 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001104 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1105 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001106
Clark Boylanb640e052014-04-03 16:41:46 -07001107 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001108 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001109 self.wait_condition.acquire()
1110 self.wait_condition.notify()
1111 self.waiting = False
1112 self.log.debug("Build %s released" % self.unique)
1113 self.wait_condition.release()
1114
1115 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001116 """Return whether this build is being held.
1117
1118 :returns: Whether the build is being held.
1119 :rtype: bool
1120 """
1121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 self.wait_condition.acquire()
1123 if self.waiting:
1124 ret = True
1125 else:
1126 ret = False
1127 self.wait_condition.release()
1128 return ret
1129
1130 def _wait(self):
1131 self.wait_condition.acquire()
1132 self.waiting = True
1133 self.log.debug("Build %s waiting" % self.unique)
1134 self.wait_condition.wait()
1135 self.wait_condition.release()
1136
1137 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.log.debug('Running build %s' % self.unique)
1139
Paul Belanger174a8272017-03-14 13:20:10 -04001140 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.log.debug('Holding build %s' % self.unique)
1142 self._wait()
1143 self.log.debug("Build %s continuing" % self.unique)
1144
James E. Blair412fba82017-01-26 15:00:50 -08001145 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001146 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001147 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001148 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001149 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001150 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001151 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001152
James E. Blaire1767bc2016-08-02 10:00:27 -07001153 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001154
James E. Blaira5dba232016-08-08 15:53:24 -07001155 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001156 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001157 for change in changes:
1158 if self.hasChanges(change):
1159 return True
1160 return False
1161
James E. Blaire7b99a02016-08-05 14:27:34 -07001162 def hasChanges(self, *changes):
1163 """Return whether this build has certain changes in its git repos.
1164
1165 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001166 are expected to be present (in order) in the git repository of
1167 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001168
1169 :returns: Whether the build has the indicated changes.
1170 :rtype: bool
1171
1172 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001173 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001174 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001175 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001176 try:
1177 repo = git.Repo(path)
1178 except NoSuchPathError as e:
1179 self.log.debug('%s' % e)
1180 return False
1181 ref = self.parameters['ZUUL_REF']
1182 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1183 commit_message = '%s-1' % change.subject
1184 self.log.debug("Checking if build %s has changes; commit_message "
1185 "%s; repo_messages %s" % (self, commit_message,
1186 repo_messages))
1187 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001188 self.log.debug(" messages do not match")
1189 return False
1190 self.log.debug(" OK")
1191 return True
1192
James E. Blaird8af5422017-05-24 13:59:40 -07001193 def getWorkspaceRepos(self, projects):
1194 """Return workspace git repo objects for the listed projects
1195
1196 :arg list projects: A list of strings, each the canonical name
1197 of a project.
1198
1199 :returns: A dictionary of {name: repo} for every listed
1200 project.
1201 :rtype: dict
1202
1203 """
1204
1205 repos = {}
1206 for project in projects:
1207 path = os.path.join(self.jobdir.src_root, project)
1208 repo = git.Repo(path)
1209 repos[project] = repo
1210 return repos
1211
Clark Boylanb640e052014-04-03 16:41:46 -07001212
Paul Belanger174a8272017-03-14 13:20:10 -04001213class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1214 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001215
Paul Belanger174a8272017-03-14 13:20:10 -04001216 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001217 they will report that they have started but then pause until
1218 released before reporting completion. This attribute may be
1219 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001220 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001221 be explicitly released.
1222
1223 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001224 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001225 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001226 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001227 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001228 self.hold_jobs_in_build = False
1229 self.lock = threading.Lock()
1230 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001231 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001232 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001233 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001234
James E. Blaira5dba232016-08-08 15:53:24 -07001235 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001236 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001237
1238 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001239 :arg Change change: The :py:class:`~tests.base.FakeChange`
1240 instance which should cause the job to fail. This job
1241 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001242
1243 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001244 l = self.fail_tests.get(name, [])
1245 l.append(change)
1246 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001247
James E. Blair962220f2016-08-03 11:22:38 -07001248 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001249 """Release a held build.
1250
1251 :arg str regex: A regular expression which, if supplied, will
1252 cause only builds with matching names to be released. If
1253 not supplied, all builds will be released.
1254
1255 """
James E. Blair962220f2016-08-03 11:22:38 -07001256 builds = self.running_builds[:]
1257 self.log.debug("Releasing build %s (%s)" % (regex,
1258 len(self.running_builds)))
1259 for build in builds:
1260 if not regex or re.match(regex, build.name):
1261 self.log.debug("Releasing build %s" %
1262 (build.parameters['ZUUL_UUID']))
1263 build.release()
1264 else:
1265 self.log.debug("Not releasing build %s" %
1266 (build.parameters['ZUUL_UUID']))
1267 self.log.debug("Done releasing builds %s (%s)" %
1268 (regex, len(self.running_builds)))
1269
Paul Belanger174a8272017-03-14 13:20:10 -04001270 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001271 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001272 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001273 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001274 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001275 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001276 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001277 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001278 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1279 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001280
1281 def stopJob(self, job):
1282 self.log.debug("handle stop")
1283 parameters = json.loads(job.arguments)
1284 uuid = parameters['uuid']
1285 for build in self.running_builds:
1286 if build.unique == uuid:
1287 build.aborted = True
1288 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001289 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001290
James E. Blaira002b032017-04-18 10:35:48 -07001291 def stop(self):
1292 for build in self.running_builds:
1293 build.release()
1294 super(RecordingExecutorServer, self).stop()
1295
Joshua Hesketh50c21782016-10-13 21:34:14 +11001296
Paul Belanger174a8272017-03-14 13:20:10 -04001297class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001298 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001299 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001300 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001301 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001302 if not commit: # merge conflict
1303 self.recordResult('MERGER_FAILURE')
1304 return commit
1305
1306 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001307 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001308 self.executor_server.lock.acquire()
1309 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001310 BuildHistory(name=build.name, result=result, changes=build.changes,
1311 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001312 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001313 pipeline=build.parameters['ZUUL_PIPELINE'])
1314 )
Paul Belanger174a8272017-03-14 13:20:10 -04001315 self.executor_server.running_builds.remove(build)
1316 del self.executor_server.job_builds[self.job.unique]
1317 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001318
1319 def runPlaybooks(self, args):
1320 build = self.executor_server.job_builds[self.job.unique]
1321 build.jobdir = self.jobdir
1322
1323 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1324 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001325 return result
1326
Monty Taylore6562aa2017-02-20 07:37:39 -05001327 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001328 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001329
Paul Belanger174a8272017-03-14 13:20:10 -04001330 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001331 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001332 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001333 else:
1334 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001335 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001336
James E. Blairad8dca02017-02-21 11:48:32 -05001337 def getHostList(self, args):
1338 self.log.debug("hostlist")
1339 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001340 for host in hosts:
1341 host['host_vars']['ansible_connection'] = 'local'
1342
1343 hosts.append(dict(
1344 name='localhost',
1345 host_vars=dict(ansible_connection='local'),
1346 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001347 return hosts
1348
James E. Blairf5dbd002015-12-23 15:26:17 -08001349
Clark Boylanb640e052014-04-03 16:41:46 -07001350class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001351 """A Gearman server for use in tests.
1352
1353 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1354 added to the queue but will not be distributed to workers
1355 until released. This attribute may be changed at any time and
1356 will take effect for subsequently enqueued jobs, but
1357 previously held jobs will still need to be explicitly
1358 released.
1359
1360 """
1361
Clark Boylanb640e052014-04-03 16:41:46 -07001362 def __init__(self):
1363 self.hold_jobs_in_queue = False
1364 super(FakeGearmanServer, self).__init__(0)
1365
1366 def getJobForConnection(self, connection, peek=False):
1367 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1368 for job in queue:
1369 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001370 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001371 job.waiting = self.hold_jobs_in_queue
1372 else:
1373 job.waiting = False
1374 if job.waiting:
1375 continue
1376 if job.name in connection.functions:
1377 if not peek:
1378 queue.remove(job)
1379 connection.related_jobs[job.handle] = job
1380 job.worker_connection = connection
1381 job.running = True
1382 return job
1383 return None
1384
1385 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001386 """Release a held job.
1387
1388 :arg str regex: A regular expression which, if supplied, will
1389 cause only jobs with matching names to be released. If
1390 not supplied, all jobs will be released.
1391 """
Clark Boylanb640e052014-04-03 16:41:46 -07001392 released = False
1393 qlen = (len(self.high_queue) + len(self.normal_queue) +
1394 len(self.low_queue))
1395 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1396 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001397 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001398 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001399 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001400 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001401 self.log.debug("releasing queued job %s" %
1402 job.unique)
1403 job.waiting = False
1404 released = True
1405 else:
1406 self.log.debug("not releasing queued job %s" %
1407 job.unique)
1408 if released:
1409 self.wakeConnections()
1410 qlen = (len(self.high_queue) + len(self.normal_queue) +
1411 len(self.low_queue))
1412 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1413
1414
1415class FakeSMTP(object):
1416 log = logging.getLogger('zuul.FakeSMTP')
1417
1418 def __init__(self, messages, server, port):
1419 self.server = server
1420 self.port = port
1421 self.messages = messages
1422
1423 def sendmail(self, from_email, to_email, msg):
1424 self.log.info("Sending email from %s, to %s, with msg %s" % (
1425 from_email, to_email, msg))
1426
1427 headers = msg.split('\n\n', 1)[0]
1428 body = msg.split('\n\n', 1)[1]
1429
1430 self.messages.append(dict(
1431 from_email=from_email,
1432 to_email=to_email,
1433 msg=msg,
1434 headers=headers,
1435 body=body,
1436 ))
1437
1438 return True
1439
1440 def quit(self):
1441 return True
1442
1443
James E. Blairdce6cea2016-12-20 16:45:32 -08001444class FakeNodepool(object):
1445 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001446 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001447
1448 log = logging.getLogger("zuul.test.FakeNodepool")
1449
1450 def __init__(self, host, port, chroot):
1451 self.client = kazoo.client.KazooClient(
1452 hosts='%s:%s%s' % (host, port, chroot))
1453 self.client.start()
1454 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001455 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001456 self.thread = threading.Thread(target=self.run)
1457 self.thread.daemon = True
1458 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001459 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001460
1461 def stop(self):
1462 self._running = False
1463 self.thread.join()
1464 self.client.stop()
1465 self.client.close()
1466
1467 def run(self):
1468 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001469 try:
1470 self._run()
1471 except Exception:
1472 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001473 time.sleep(0.1)
1474
1475 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001476 if self.paused:
1477 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001478 for req in self.getNodeRequests():
1479 self.fulfillRequest(req)
1480
1481 def getNodeRequests(self):
1482 try:
1483 reqids = self.client.get_children(self.REQUEST_ROOT)
1484 except kazoo.exceptions.NoNodeError:
1485 return []
1486 reqs = []
1487 for oid in sorted(reqids):
1488 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001489 try:
1490 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001491 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001492 data['_oid'] = oid
1493 reqs.append(data)
1494 except kazoo.exceptions.NoNodeError:
1495 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001496 return reqs
1497
James E. Blaire18d4602017-01-05 11:17:28 -08001498 def getNodes(self):
1499 try:
1500 nodeids = self.client.get_children(self.NODE_ROOT)
1501 except kazoo.exceptions.NoNodeError:
1502 return []
1503 nodes = []
1504 for oid in sorted(nodeids):
1505 path = self.NODE_ROOT + '/' + oid
1506 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001507 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001508 data['_oid'] = oid
1509 try:
1510 lockfiles = self.client.get_children(path + '/lock')
1511 except kazoo.exceptions.NoNodeError:
1512 lockfiles = []
1513 if lockfiles:
1514 data['_lock'] = True
1515 else:
1516 data['_lock'] = False
1517 nodes.append(data)
1518 return nodes
1519
James E. Blaira38c28e2017-01-04 10:33:20 -08001520 def makeNode(self, request_id, node_type):
1521 now = time.time()
1522 path = '/nodepool/nodes/'
1523 data = dict(type=node_type,
1524 provider='test-provider',
1525 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001526 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001527 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001528 public_ipv4='127.0.0.1',
1529 private_ipv4=None,
1530 public_ipv6=None,
1531 allocated_to=request_id,
1532 state='ready',
1533 state_time=now,
1534 created_time=now,
1535 updated_time=now,
1536 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001537 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001538 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001539 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001540 path = self.client.create(path, data,
1541 makepath=True,
1542 sequence=True)
1543 nodeid = path.split("/")[-1]
1544 return nodeid
1545
James E. Blair6ab79e02017-01-06 10:10:17 -08001546 def addFailRequest(self, request):
1547 self.fail_requests.add(request['_oid'])
1548
James E. Blairdce6cea2016-12-20 16:45:32 -08001549 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001550 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001551 return
1552 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001553 oid = request['_oid']
1554 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001555
James E. Blair6ab79e02017-01-06 10:10:17 -08001556 if oid in self.fail_requests:
1557 request['state'] = 'failed'
1558 else:
1559 request['state'] = 'fulfilled'
1560 nodes = []
1561 for node in request['node_types']:
1562 nodeid = self.makeNode(oid, node)
1563 nodes.append(nodeid)
1564 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001565
James E. Blaira38c28e2017-01-04 10:33:20 -08001566 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001567 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001568 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001569 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001570 try:
1571 self.client.set(path, data)
1572 except kazoo.exceptions.NoNodeError:
1573 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001574
1575
James E. Blair498059b2016-12-20 13:50:13 -08001576class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001577 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001578 super(ChrootedKazooFixture, self).__init__()
1579
1580 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1581 if ':' in zk_host:
1582 host, port = zk_host.split(':')
1583 else:
1584 host = zk_host
1585 port = None
1586
1587 self.zookeeper_host = host
1588
1589 if not port:
1590 self.zookeeper_port = 2181
1591 else:
1592 self.zookeeper_port = int(port)
1593
Clark Boylan621ec9a2017-04-07 17:41:33 -07001594 self.test_id = test_id
1595
James E. Blair498059b2016-12-20 13:50:13 -08001596 def _setUp(self):
1597 # Make sure the test chroot paths do not conflict
1598 random_bits = ''.join(random.choice(string.ascii_lowercase +
1599 string.ascii_uppercase)
1600 for x in range(8))
1601
Clark Boylan621ec9a2017-04-07 17:41:33 -07001602 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001603 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1604
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001605 self.addCleanup(self._cleanup)
1606
James E. Blair498059b2016-12-20 13:50:13 -08001607 # Ensure the chroot path exists and clean up any pre-existing znodes.
1608 _tmp_client = kazoo.client.KazooClient(
1609 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1610 _tmp_client.start()
1611
1612 if _tmp_client.exists(self.zookeeper_chroot):
1613 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1614
1615 _tmp_client.ensure_path(self.zookeeper_chroot)
1616 _tmp_client.stop()
1617 _tmp_client.close()
1618
James E. Blair498059b2016-12-20 13:50:13 -08001619 def _cleanup(self):
1620 '''Remove the chroot path.'''
1621 # Need a non-chroot'ed client to remove the chroot path
1622 _tmp_client = kazoo.client.KazooClient(
1623 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1624 _tmp_client.start()
1625 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1626 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001627 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001628
1629
Joshua Heskethd78b4482015-09-14 16:56:34 -06001630class MySQLSchemaFixture(fixtures.Fixture):
1631 def setUp(self):
1632 super(MySQLSchemaFixture, self).setUp()
1633
1634 random_bits = ''.join(random.choice(string.ascii_lowercase +
1635 string.ascii_uppercase)
1636 for x in range(8))
1637 self.name = '%s_%s' % (random_bits, os.getpid())
1638 self.passwd = uuid.uuid4().hex
1639 db = pymysql.connect(host="localhost",
1640 user="openstack_citest",
1641 passwd="openstack_citest",
1642 db="openstack_citest")
1643 cur = db.cursor()
1644 cur.execute("create database %s" % self.name)
1645 cur.execute(
1646 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1647 (self.name, self.name, self.passwd))
1648 cur.execute("flush privileges")
1649
1650 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1651 self.passwd,
1652 self.name)
1653 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1654 self.addCleanup(self.cleanup)
1655
1656 def cleanup(self):
1657 db = pymysql.connect(host="localhost",
1658 user="openstack_citest",
1659 passwd="openstack_citest",
1660 db="openstack_citest")
1661 cur = db.cursor()
1662 cur.execute("drop database %s" % self.name)
1663 cur.execute("drop user '%s'@'localhost'" % self.name)
1664 cur.execute("flush privileges")
1665
1666
Maru Newby3fe5f852015-01-13 04:22:14 +00001667class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001668 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001669 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001670
James E. Blair1c236df2017-02-01 14:07:24 -08001671 def attachLogs(self, *args):
1672 def reader():
1673 self._log_stream.seek(0)
1674 while True:
1675 x = self._log_stream.read(4096)
1676 if not x:
1677 break
1678 yield x.encode('utf8')
1679 content = testtools.content.content_from_reader(
1680 reader,
1681 testtools.content_type.UTF8_TEXT,
1682 False)
1683 self.addDetail('logging', content)
1684
Clark Boylanb640e052014-04-03 16:41:46 -07001685 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001686 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001687 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1688 try:
1689 test_timeout = int(test_timeout)
1690 except ValueError:
1691 # If timeout value is invalid do not set a timeout.
1692 test_timeout = 0
1693 if test_timeout > 0:
1694 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1695
1696 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1697 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1698 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1699 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1700 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1701 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1702 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1703 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1704 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1705 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001706 self._log_stream = StringIO()
1707 self.addOnException(self.attachLogs)
1708 else:
1709 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001710
James E. Blair73b41772017-05-22 13:22:55 -07001711 # NOTE(jeblair): this is temporary extra debugging to try to
1712 # track down a possible leak.
1713 orig_git_repo_init = git.Repo.__init__
1714
1715 def git_repo_init(myself, *args, **kw):
1716 orig_git_repo_init(myself, *args, **kw)
1717 self.log.debug("Created git repo 0x%x %s" %
1718 (id(myself), repr(myself)))
1719
1720 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1721 git_repo_init))
1722
James E. Blair1c236df2017-02-01 14:07:24 -08001723 handler = logging.StreamHandler(self._log_stream)
1724 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1725 '%(levelname)-8s %(message)s')
1726 handler.setFormatter(formatter)
1727
1728 logger = logging.getLogger()
1729 logger.setLevel(logging.DEBUG)
1730 logger.addHandler(handler)
1731
Clark Boylan3410d532017-04-25 12:35:29 -07001732 # Make sure we don't carry old handlers around in process state
1733 # which slows down test runs
1734 self.addCleanup(logger.removeHandler, handler)
1735 self.addCleanup(handler.close)
1736 self.addCleanup(handler.flush)
1737
James E. Blair1c236df2017-02-01 14:07:24 -08001738 # NOTE(notmorgan): Extract logging overrides for specific
1739 # libraries from the OS_LOG_DEFAULTS env and create loggers
1740 # for each. This is used to limit the output during test runs
1741 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001742 log_defaults_from_env = os.environ.get(
1743 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001744 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001745
James E. Blairdce6cea2016-12-20 16:45:32 -08001746 if log_defaults_from_env:
1747 for default in log_defaults_from_env.split(','):
1748 try:
1749 name, level_str = default.split('=', 1)
1750 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001751 logger = logging.getLogger(name)
1752 logger.setLevel(level)
1753 logger.addHandler(handler)
1754 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001755 except ValueError:
1756 # NOTE(notmorgan): Invalid format of the log default,
1757 # skip and don't try and apply a logger for the
1758 # specified module
1759 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001760
Maru Newby3fe5f852015-01-13 04:22:14 +00001761
1762class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001763 """A test case with a functioning Zuul.
1764
1765 The following class variables are used during test setup and can
1766 be overidden by subclasses but are effectively read-only once a
1767 test method starts running:
1768
1769 :cvar str config_file: This points to the main zuul config file
1770 within the fixtures directory. Subclasses may override this
1771 to obtain a different behavior.
1772
1773 :cvar str tenant_config_file: This is the tenant config file
1774 (which specifies from what git repos the configuration should
1775 be loaded). It defaults to the value specified in
1776 `config_file` but can be overidden by subclasses to obtain a
1777 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001778 configuration. See also the :py:func:`simple_layout`
1779 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001780
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001781 :cvar bool create_project_keys: Indicates whether Zuul should
1782 auto-generate keys for each project, or whether the test
1783 infrastructure should insert dummy keys to save time during
1784 startup. Defaults to False.
1785
James E. Blaire7b99a02016-08-05 14:27:34 -07001786 The following are instance variables that are useful within test
1787 methods:
1788
1789 :ivar FakeGerritConnection fake_<connection>:
1790 A :py:class:`~tests.base.FakeGerritConnection` will be
1791 instantiated for each connection present in the config file
1792 and stored here. For instance, `fake_gerrit` will hold the
1793 FakeGerritConnection object for a connection named `gerrit`.
1794
1795 :ivar FakeGearmanServer gearman_server: An instance of
1796 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1797 server that all of the Zuul components in this test use to
1798 communicate with each other.
1799
Paul Belanger174a8272017-03-14 13:20:10 -04001800 :ivar RecordingExecutorServer executor_server: An instance of
1801 :py:class:`~tests.base.RecordingExecutorServer` which is the
1802 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001803
1804 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1805 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001806 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001807 list upon completion.
1808
1809 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1810 objects representing completed builds. They are appended to
1811 the list in the order they complete.
1812
1813 """
1814
James E. Blair83005782015-12-11 14:46:03 -08001815 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001816 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001817 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001818
1819 def _startMerger(self):
1820 self.merge_server = zuul.merger.server.MergeServer(self.config,
1821 self.connections)
1822 self.merge_server.start()
1823
Maru Newby3fe5f852015-01-13 04:22:14 +00001824 def setUp(self):
1825 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001826
1827 self.setupZK()
1828
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001829 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001830 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001831 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1832 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001833 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001834 tmp_root = tempfile.mkdtemp(
1835 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001836 self.test_root = os.path.join(tmp_root, "zuul-test")
1837 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001838 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001839 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001840 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001841
1842 if os.path.exists(self.test_root):
1843 shutil.rmtree(self.test_root)
1844 os.makedirs(self.test_root)
1845 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001846 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001847
1848 # Make per test copy of Configuration.
1849 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001850 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1851 if not os.path.exists(self.private_key_file):
1852 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1853 shutil.copy(src_private_key_file, self.private_key_file)
1854 shutil.copy('{}.pub'.format(src_private_key_file),
1855 '{}.pub'.format(self.private_key_file))
1856 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001857 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001858 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001859 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001860 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001861 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001862 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001863 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001864
Clark Boylanb640e052014-04-03 16:41:46 -07001865 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001866 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1867 # see: https://github.com/jsocol/pystatsd/issues/61
1868 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001869 os.environ['STATSD_PORT'] = str(self.statsd.port)
1870 self.statsd.start()
1871 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001872 reload_module(statsd)
1873 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001874
1875 self.gearman_server = FakeGearmanServer()
1876
1877 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001878 self.log.info("Gearman server on port %s" %
1879 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001880
James E. Blaire511d2f2016-12-08 15:22:26 -08001881 gerritsource.GerritSource.replication_timeout = 1.5
1882 gerritsource.GerritSource.replication_retry_interval = 0.5
1883 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001884
Joshua Hesketh352264b2015-08-11 23:42:08 +10001885 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001886
Jan Hruban7083edd2015-08-21 14:00:54 +02001887 self.webapp = zuul.webapp.WebApp(
1888 self.sched, port=0, listen_address='127.0.0.1')
1889
Jan Hruban6b71aff2015-10-22 16:58:08 +02001890 self.event_queues = [
1891 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001892 self.sched.trigger_event_queue,
1893 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001894 ]
1895
James E. Blairfef78942016-03-11 16:28:56 -08001896 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001897 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001898
Paul Belanger174a8272017-03-14 13:20:10 -04001899 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001900 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001901 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001902 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001903 _test_root=self.test_root,
1904 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001905 self.executor_server.start()
1906 self.history = self.executor_server.build_history
1907 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001908
Paul Belanger174a8272017-03-14 13:20:10 -04001909 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001910 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001911 self.merge_client = zuul.merger.client.MergeClient(
1912 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001913 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001914 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001915 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001916
James E. Blair0d5a36e2017-02-21 10:53:44 -05001917 self.fake_nodepool = FakeNodepool(
1918 self.zk_chroot_fixture.zookeeper_host,
1919 self.zk_chroot_fixture.zookeeper_port,
1920 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001921
Paul Belanger174a8272017-03-14 13:20:10 -04001922 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001923 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001924 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001925 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001926
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001927 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001928
1929 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001930 self.webapp.start()
1931 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001932 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001933 # Cleanups are run in reverse order
1934 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001935 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001936 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001937
James E. Blairb9c0d772017-03-03 14:34:49 -08001938 self.sched.reconfigure(self.config)
1939 self.sched.resume()
1940
Tobias Henkel7df274b2017-05-26 17:41:11 +02001941 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001942 # Set up gerrit related fakes
1943 # Set a changes database so multiple FakeGerrit's can report back to
1944 # a virtual canonical database given by the configured hostname
1945 self.gerrit_changes_dbs = {}
1946
1947 def getGerritConnection(driver, name, config):
1948 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1949 con = FakeGerritConnection(driver, name, config,
1950 changes_db=db,
1951 upstream_root=self.upstream_root)
1952 self.event_queues.append(con.event_queue)
1953 setattr(self, 'fake_' + name, con)
1954 return con
1955
1956 self.useFixture(fixtures.MonkeyPatch(
1957 'zuul.driver.gerrit.GerritDriver.getConnection',
1958 getGerritConnection))
1959
Gregory Haynes4fc12542015-04-22 20:38:06 -07001960 def getGithubConnection(driver, name, config):
1961 con = FakeGithubConnection(driver, name, config,
1962 upstream_root=self.upstream_root)
1963 setattr(self, 'fake_' + name, con)
1964 return con
1965
1966 self.useFixture(fixtures.MonkeyPatch(
1967 'zuul.driver.github.GithubDriver.getConnection',
1968 getGithubConnection))
1969
James E. Blaire511d2f2016-12-08 15:22:26 -08001970 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001971 # TODO(jhesketh): This should come from lib.connections for better
1972 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001973 # Register connections from the config
1974 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001975
Joshua Hesketh352264b2015-08-11 23:42:08 +10001976 def FakeSMTPFactory(*args, **kw):
1977 args = [self.smtp_messages] + list(args)
1978 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001979
Joshua Hesketh352264b2015-08-11 23:42:08 +10001980 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001981
James E. Blaire511d2f2016-12-08 15:22:26 -08001982 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001983 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02001984 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001985
James E. Blair83005782015-12-11 14:46:03 -08001986 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001987 # This creates the per-test configuration object. It can be
1988 # overriden by subclasses, but should not need to be since it
1989 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001990 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001991 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001992
1993 if not self.setupSimpleLayout():
1994 if hasattr(self, 'tenant_config_file'):
1995 self.config.set('zuul', 'tenant_config',
1996 self.tenant_config_file)
1997 git_path = os.path.join(
1998 os.path.dirname(
1999 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2000 'git')
2001 if os.path.exists(git_path):
2002 for reponame in os.listdir(git_path):
2003 project = reponame.replace('_', '/')
2004 self.copyDirToRepo(project,
2005 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002006 self.setupAllProjectKeys()
2007
James E. Blair06cc3922017-04-19 10:08:10 -07002008 def setupSimpleLayout(self):
2009 # If the test method has been decorated with a simple_layout,
2010 # use that instead of the class tenant_config_file. Set up a
2011 # single config-project with the specified layout, and
2012 # initialize repos for all of the 'project' entries which
2013 # appear in the layout.
2014 test_name = self.id().split('.')[-1]
2015 test = getattr(self, test_name)
2016 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002017 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002018 else:
2019 return False
2020
James E. Blairb70e55a2017-04-19 12:57:02 -07002021 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002022 path = os.path.join(FIXTURE_DIR, path)
2023 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002024 data = f.read()
2025 layout = yaml.safe_load(data)
2026 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002027 untrusted_projects = []
2028 for item in layout:
2029 if 'project' in item:
2030 name = item['project']['name']
2031 untrusted_projects.append(name)
2032 self.init_repo(name)
2033 self.addCommitToRepo(name, 'initial commit',
2034 files={'README': ''},
2035 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002036 if 'job' in item:
2037 jobname = item['job']['name']
2038 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002039
2040 root = os.path.join(self.test_root, "config")
2041 if not os.path.exists(root):
2042 os.makedirs(root)
2043 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2044 config = [{'tenant':
2045 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002046 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002047 {'config-projects': ['common-config'],
2048 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002049 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002050 f.close()
2051 self.config.set('zuul', 'tenant_config',
2052 os.path.join(FIXTURE_DIR, f.name))
2053
2054 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002055 self.addCommitToRepo('common-config', 'add content from fixture',
2056 files, branch='master', tag='init')
2057
2058 return True
2059
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002060 def setupAllProjectKeys(self):
2061 if self.create_project_keys:
2062 return
2063
2064 path = self.config.get('zuul', 'tenant_config')
2065 with open(os.path.join(FIXTURE_DIR, path)) as f:
2066 tenant_config = yaml.safe_load(f.read())
2067 for tenant in tenant_config:
2068 sources = tenant['tenant']['source']
2069 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002070 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002071 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002072 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002073 self.setupProjectKeys(source, project)
2074
2075 def setupProjectKeys(self, source, project):
2076 # Make sure we set up an RSA key for the project so that we
2077 # don't spend time generating one:
2078
2079 key_root = os.path.join(self.state_root, 'keys')
2080 if not os.path.isdir(key_root):
2081 os.mkdir(key_root, 0o700)
2082 private_key_file = os.path.join(key_root, source, project + '.pem')
2083 private_key_dir = os.path.dirname(private_key_file)
2084 self.log.debug("Installing test keys for project %s at %s" % (
2085 project, private_key_file))
2086 if not os.path.isdir(private_key_dir):
2087 os.makedirs(private_key_dir)
2088 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2089 with open(private_key_file, 'w') as o:
2090 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002091
James E. Blair498059b2016-12-20 13:50:13 -08002092 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002093 self.zk_chroot_fixture = self.useFixture(
2094 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002095 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002096 self.zk_chroot_fixture.zookeeper_host,
2097 self.zk_chroot_fixture.zookeeper_port,
2098 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002099
James E. Blair96c6bf82016-01-15 16:20:40 -08002100 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002101 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002102
2103 files = {}
2104 for (dirpath, dirnames, filenames) in os.walk(source_path):
2105 for filename in filenames:
2106 test_tree_filepath = os.path.join(dirpath, filename)
2107 common_path = os.path.commonprefix([test_tree_filepath,
2108 source_path])
2109 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2110 with open(test_tree_filepath, 'r') as f:
2111 content = f.read()
2112 files[relative_filepath] = content
2113 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002114 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002115
James E. Blaire18d4602017-01-05 11:17:28 -08002116 def assertNodepoolState(self):
2117 # Make sure that there are no pending requests
2118
2119 requests = self.fake_nodepool.getNodeRequests()
2120 self.assertEqual(len(requests), 0)
2121
2122 nodes = self.fake_nodepool.getNodes()
2123 for node in nodes:
2124 self.assertFalse(node['_lock'], "Node %s is locked" %
2125 (node['_oid'],))
2126
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002127 def assertNoGeneratedKeys(self):
2128 # Make sure that Zuul did not generate any project keys
2129 # (unless it was supposed to).
2130
2131 if self.create_project_keys:
2132 return
2133
2134 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2135 test_key = i.read()
2136
2137 key_root = os.path.join(self.state_root, 'keys')
2138 for root, dirname, files in os.walk(key_root):
2139 for fn in files:
2140 with open(os.path.join(root, fn)) as f:
2141 self.assertEqual(test_key, f.read())
2142
Clark Boylanb640e052014-04-03 16:41:46 -07002143 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002144 self.log.debug("Assert final state")
2145 # Make sure no jobs are running
2146 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002147 # Make sure that git.Repo objects have been garbage collected.
2148 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002149 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002150 gc.collect()
2151 for obj in gc.get_objects():
2152 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002153 self.log.debug("Leaked git repo object: 0x%x %s" %
2154 (id(obj), repr(obj)))
2155 for ref in gc.get_referrers(obj):
2156 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002157 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002158 if repos:
2159 for obj in gc.garbage:
2160 self.log.debug(" Garbage %s" % (repr(obj)))
2161 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002162 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002163 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002164 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002165 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002166 for tenant in self.sched.abide.tenants.values():
2167 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002168 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002169 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002170
2171 def shutdown(self):
2172 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002173 self.executor_server.hold_jobs_in_build = False
2174 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002175 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002176 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002177 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002178 self.sched.stop()
2179 self.sched.join()
2180 self.statsd.stop()
2181 self.statsd.join()
2182 self.webapp.stop()
2183 self.webapp.join()
2184 self.rpc.stop()
2185 self.rpc.join()
2186 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002187 self.fake_nodepool.stop()
2188 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002189 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002190 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002191 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002192 # Further the pydevd threads also need to be whitelisted so debugging
2193 # e.g. in PyCharm is possible without breaking shutdown.
2194 whitelist = ['executor-watchdog',
2195 'pydevd.CommandThread',
2196 'pydevd.Reader',
2197 'pydevd.Writer',
2198 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002199 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002200 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002201 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002202 log_str = ""
2203 for thread_id, stack_frame in sys._current_frames().items():
2204 log_str += "Thread: %s\n" % thread_id
2205 log_str += "".join(traceback.format_stack(stack_frame))
2206 self.log.debug(log_str)
2207 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002208
James E. Blaira002b032017-04-18 10:35:48 -07002209 def assertCleanShutdown(self):
2210 pass
2211
James E. Blairc4ba97a2017-04-19 16:26:24 -07002212 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002213 parts = project.split('/')
2214 path = os.path.join(self.upstream_root, *parts[:-1])
2215 if not os.path.exists(path):
2216 os.makedirs(path)
2217 path = os.path.join(self.upstream_root, project)
2218 repo = git.Repo.init(path)
2219
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002220 with repo.config_writer() as config_writer:
2221 config_writer.set_value('user', 'email', 'user@example.com')
2222 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002223
Clark Boylanb640e052014-04-03 16:41:46 -07002224 repo.index.commit('initial commit')
2225 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002226 if tag:
2227 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002228
James E. Blair97d902e2014-08-21 13:25:56 -07002229 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002230 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002231 repo.git.clean('-x', '-f', '-d')
2232
James E. Blair97d902e2014-08-21 13:25:56 -07002233 def create_branch(self, project, branch):
2234 path = os.path.join(self.upstream_root, project)
2235 repo = git.Repo.init(path)
2236 fn = os.path.join(path, 'README')
2237
2238 branch_head = repo.create_head(branch)
2239 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002240 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002241 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002242 f.close()
2243 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002244 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002245
James E. Blair97d902e2014-08-21 13:25:56 -07002246 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002247 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002248 repo.git.clean('-x', '-f', '-d')
2249
Sachi King9f16d522016-03-16 12:20:45 +11002250 def create_commit(self, project):
2251 path = os.path.join(self.upstream_root, project)
2252 repo = git.Repo(path)
2253 repo.head.reference = repo.heads['master']
2254 file_name = os.path.join(path, 'README')
2255 with open(file_name, 'a') as f:
2256 f.write('creating fake commit\n')
2257 repo.index.add([file_name])
2258 commit = repo.index.commit('Creating a fake commit')
2259 return commit.hexsha
2260
James E. Blairf4a5f022017-04-18 14:01:10 -07002261 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002262 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002263 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002264 while len(self.builds):
2265 self.release(self.builds[0])
2266 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002267 i += 1
2268 if count is not None and i >= count:
2269 break
James E. Blairb8c16472015-05-05 14:55:26 -07002270
Clark Boylanb640e052014-04-03 16:41:46 -07002271 def release(self, job):
2272 if isinstance(job, FakeBuild):
2273 job.release()
2274 else:
2275 job.waiting = False
2276 self.log.debug("Queued job %s released" % job.unique)
2277 self.gearman_server.wakeConnections()
2278
2279 def getParameter(self, job, name):
2280 if isinstance(job, FakeBuild):
2281 return job.parameters[name]
2282 else:
2283 parameters = json.loads(job.arguments)
2284 return parameters[name]
2285
Clark Boylanb640e052014-04-03 16:41:46 -07002286 def haveAllBuildsReported(self):
2287 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002288 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002289 return False
2290 # Find out if every build that the worker has completed has been
2291 # reported back to Zuul. If it hasn't then that means a Gearman
2292 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002293 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002294 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002295 if not zbuild:
2296 # It has already been reported
2297 continue
2298 # It hasn't been reported yet.
2299 return False
2300 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002301 worker = self.executor_server.executor_worker
2302 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002303 if connection.state == 'GRAB_WAIT':
2304 return False
2305 return True
2306
2307 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002308 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002309 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002310 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002311 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002312 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002313 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002314 for j in conn.related_jobs.values():
2315 if j.unique == build.uuid:
2316 client_job = j
2317 break
2318 if not client_job:
2319 self.log.debug("%s is not known to the gearman client" %
2320 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002321 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002322 if not client_job.handle:
2323 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002324 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002325 server_job = self.gearman_server.jobs.get(client_job.handle)
2326 if not server_job:
2327 self.log.debug("%s is not known to the gearman server" %
2328 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002329 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002330 if not hasattr(server_job, 'waiting'):
2331 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002332 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002333 if server_job.waiting:
2334 continue
James E. Blair17302972016-08-10 16:11:42 -07002335 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002336 self.log.debug("%s has not reported start" % build)
2337 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002338 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002339 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002340 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002341 if worker_build:
2342 if worker_build.isWaiting():
2343 continue
2344 else:
2345 self.log.debug("%s is running" % worker_build)
2346 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002347 else:
James E. Blair962220f2016-08-03 11:22:38 -07002348 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002349 return False
James E. Blaira002b032017-04-18 10:35:48 -07002350 for (build_uuid, job_worker) in \
2351 self.executor_server.job_workers.items():
2352 if build_uuid not in seen_builds:
2353 self.log.debug("%s is not finalized" % build_uuid)
2354 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002355 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002356
James E. Blairdce6cea2016-12-20 16:45:32 -08002357 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002358 if self.fake_nodepool.paused:
2359 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002360 if self.sched.nodepool.requests:
2361 return False
2362 return True
2363
Jan Hruban6b71aff2015-10-22 16:58:08 +02002364 def eventQueuesEmpty(self):
2365 for queue in self.event_queues:
2366 yield queue.empty()
2367
2368 def eventQueuesJoin(self):
2369 for queue in self.event_queues:
2370 queue.join()
2371
Clark Boylanb640e052014-04-03 16:41:46 -07002372 def waitUntilSettled(self):
2373 self.log.debug("Waiting until settled...")
2374 start = time.time()
2375 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002376 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002377 self.log.error("Timeout waiting for Zuul to settle")
2378 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002379 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002380 self.log.error(" %s: %s" % (queue, queue.empty()))
2381 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002382 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002383 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002384 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002385 self.log.error("All requests completed: %s" %
2386 (self.areAllNodeRequestsComplete(),))
2387 self.log.error("Merge client jobs: %s" %
2388 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002389 raise Exception("Timeout waiting for Zuul to settle")
2390 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002391
Paul Belanger174a8272017-03-14 13:20:10 -04002392 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002393 # have all build states propogated to zuul?
2394 if self.haveAllBuildsReported():
2395 # Join ensures that the queue is empty _and_ events have been
2396 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002397 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002398 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002399 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002400 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002401 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002402 self.areAllNodeRequestsComplete() and
2403 all(self.eventQueuesEmpty())):
2404 # The queue empty check is placed at the end to
2405 # ensure that if a component adds an event between
2406 # when locked the run handler and checked that the
2407 # components were stable, we don't erroneously
2408 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002409 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002410 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002411 self.log.debug("...settled.")
2412 return
2413 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002414 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002415 self.sched.wake_event.wait(0.1)
2416
2417 def countJobResults(self, jobs, result):
2418 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002419 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002420
Monty Taylor0d926122017-05-24 08:07:56 -05002421 def getBuildByName(self, name):
2422 for build in self.builds:
2423 if build.name == name:
2424 return build
2425 raise Exception("Unable to find build %s" % name)
2426
James E. Blair96c6bf82016-01-15 16:20:40 -08002427 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002428 for job in self.history:
2429 if (job.name == name and
2430 (project is None or
2431 job.parameters['ZUUL_PROJECT'] == project)):
2432 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002433 raise Exception("Unable to find job %s in history" % name)
2434
2435 def assertEmptyQueues(self):
2436 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002437 for tenant in self.sched.abide.tenants.values():
2438 for pipeline in tenant.layout.pipelines.values():
2439 for queue in pipeline.queues:
2440 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002441 print('pipeline %s queue %s contents %s' % (
2442 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002443 self.assertEqual(len(queue.queue), 0,
2444 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002445
2446 def assertReportedStat(self, key, value=None, kind=None):
2447 start = time.time()
2448 while time.time() < (start + 5):
2449 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002450 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002451 if key == k:
2452 if value is None and kind is None:
2453 return
2454 elif value:
2455 if value == v:
2456 return
2457 elif kind:
2458 if v.endswith('|' + kind):
2459 return
2460 time.sleep(0.1)
2461
Clark Boylanb640e052014-04-03 16:41:46 -07002462 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002463
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002464 def assertBuilds(self, builds):
2465 """Assert that the running builds are as described.
2466
2467 The list of running builds is examined and must match exactly
2468 the list of builds described by the input.
2469
2470 :arg list builds: A list of dictionaries. Each item in the
2471 list must match the corresponding build in the build
2472 history, and each element of the dictionary must match the
2473 corresponding attribute of the build.
2474
2475 """
James E. Blair3158e282016-08-19 09:34:11 -07002476 try:
2477 self.assertEqual(len(self.builds), len(builds))
2478 for i, d in enumerate(builds):
2479 for k, v in d.items():
2480 self.assertEqual(
2481 getattr(self.builds[i], k), v,
2482 "Element %i in builds does not match" % (i,))
2483 except Exception:
2484 for build in self.builds:
2485 self.log.error("Running build: %s" % build)
2486 else:
2487 self.log.error("No running builds")
2488 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002489
James E. Blairb536ecc2016-08-31 10:11:42 -07002490 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002491 """Assert that the completed builds are as described.
2492
2493 The list of completed builds is examined and must match
2494 exactly the list of builds described by the input.
2495
2496 :arg list history: A list of dictionaries. Each item in the
2497 list must match the corresponding build in the build
2498 history, and each element of the dictionary must match the
2499 corresponding attribute of the build.
2500
James E. Blairb536ecc2016-08-31 10:11:42 -07002501 :arg bool ordered: If true, the history must match the order
2502 supplied, if false, the builds are permitted to have
2503 arrived in any order.
2504
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002505 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002506 def matches(history_item, item):
2507 for k, v in item.items():
2508 if getattr(history_item, k) != v:
2509 return False
2510 return True
James E. Blair3158e282016-08-19 09:34:11 -07002511 try:
2512 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002513 if ordered:
2514 for i, d in enumerate(history):
2515 if not matches(self.history[i], d):
2516 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002517 "Element %i in history does not match %s" %
2518 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002519 else:
2520 unseen = self.history[:]
2521 for i, d in enumerate(history):
2522 found = False
2523 for unseen_item in unseen:
2524 if matches(unseen_item, d):
2525 found = True
2526 unseen.remove(unseen_item)
2527 break
2528 if not found:
2529 raise Exception("No match found for element %i "
2530 "in history" % (i,))
2531 if unseen:
2532 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002533 except Exception:
2534 for build in self.history:
2535 self.log.error("Completed build: %s" % build)
2536 else:
2537 self.log.error("No completed builds")
2538 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002539
James E. Blair6ac368c2016-12-22 18:07:20 -08002540 def printHistory(self):
2541 """Log the build history.
2542
2543 This can be useful during tests to summarize what jobs have
2544 completed.
2545
2546 """
2547 self.log.debug("Build history:")
2548 for build in self.history:
2549 self.log.debug(build)
2550
James E. Blair59fdbac2015-12-07 17:08:06 -08002551 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002552 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2553
James E. Blair9ea70072017-04-19 16:05:30 -07002554 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002555 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002556 if not os.path.exists(root):
2557 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002558 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2559 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002560- tenant:
2561 name: openstack
2562 source:
2563 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002564 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002565 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002566 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002567 - org/project
2568 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002569 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002570 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002571 self.config.set('zuul', 'tenant_config',
2572 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002573 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002574
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002575 def addCommitToRepo(self, project, message, files,
2576 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002577 path = os.path.join(self.upstream_root, project)
2578 repo = git.Repo(path)
2579 repo.head.reference = branch
2580 zuul.merger.merger.reset_repo_to_head(repo)
2581 for fn, content in files.items():
2582 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002583 try:
2584 os.makedirs(os.path.dirname(fn))
2585 except OSError:
2586 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002587 with open(fn, 'w') as f:
2588 f.write(content)
2589 repo.index.add([fn])
2590 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002591 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002592 repo.heads[branch].commit = commit
2593 repo.head.reference = branch
2594 repo.git.clean('-x', '-f', '-d')
2595 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002596 if tag:
2597 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002598 return before
2599
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002600 def commitConfigUpdate(self, project_name, source_name):
2601 """Commit an update to zuul.yaml
2602
2603 This overwrites the zuul.yaml in the specificed project with
2604 the contents specified.
2605
2606 :arg str project_name: The name of the project containing
2607 zuul.yaml (e.g., common-config)
2608
2609 :arg str source_name: The path to the file (underneath the
2610 test fixture directory) whose contents should be used to
2611 replace zuul.yaml.
2612 """
2613
2614 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002615 files = {}
2616 with open(source_path, 'r') as f:
2617 data = f.read()
2618 layout = yaml.safe_load(data)
2619 files['zuul.yaml'] = data
2620 for item in layout:
2621 if 'job' in item:
2622 jobname = item['job']['name']
2623 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002624 before = self.addCommitToRepo(
2625 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002626 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002627 return before
2628
James E. Blair7fc8daa2016-08-08 15:37:15 -07002629 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002630
James E. Blair7fc8daa2016-08-08 15:37:15 -07002631 """Inject a Fake (Gerrit) event.
2632
2633 This method accepts a JSON-encoded event and simulates Zuul
2634 having received it from Gerrit. It could (and should)
2635 eventually apply to any connection type, but is currently only
2636 used with Gerrit connections. The name of the connection is
2637 used to look up the corresponding server, and the event is
2638 simulated as having been received by all Zuul connections
2639 attached to that server. So if two Gerrit connections in Zuul
2640 are connected to the same Gerrit server, and you invoke this
2641 method specifying the name of one of them, the event will be
2642 received by both.
2643
2644 .. note::
2645
2646 "self.fake_gerrit.addEvent" calls should be migrated to
2647 this method.
2648
2649 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002650 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002651 :arg str event: The JSON-encoded event.
2652
2653 """
2654 specified_conn = self.connections.connections[connection]
2655 for conn in self.connections.connections.values():
2656 if (isinstance(conn, specified_conn.__class__) and
2657 specified_conn.server == conn.server):
2658 conn.addEvent(event)
2659
James E. Blaird8af5422017-05-24 13:59:40 -07002660 def getUpstreamRepos(self, projects):
2661 """Return upstream git repo objects for the listed projects
2662
2663 :arg list projects: A list of strings, each the canonical name
2664 of a project.
2665
2666 :returns: A dictionary of {name: repo} for every listed
2667 project.
2668 :rtype: dict
2669
2670 """
2671
2672 repos = {}
2673 for project in projects:
2674 # FIXME(jeblair): the upstream root does not yet have a
2675 # hostname component; that needs to be added, and this
2676 # line removed:
2677 tmp_project_name = '/'.join(project.split('/')[1:])
2678 path = os.path.join(self.upstream_root, tmp_project_name)
2679 repo = git.Repo(path)
2680 repos[project] = repo
2681 return repos
2682
James E. Blair3f876d52016-07-22 13:07:14 -07002683
2684class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002685 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002686 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002687
Joshua Heskethd78b4482015-09-14 16:56:34 -06002688
2689class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002690 def setup_config(self):
2691 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002692 for section_name in self.config.sections():
2693 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2694 section_name, re.I)
2695 if not con_match:
2696 continue
2697
2698 if self.config.get(section_name, 'driver') == 'sql':
2699 f = MySQLSchemaFixture()
2700 self.useFixture(f)
2701 if (self.config.get(section_name, 'dburi') ==
2702 '$MYSQL_FIXTURE_DBURI$'):
2703 self.config.set(section_name, 'dburi', f.dburi)