blob: 76f7e03b5d457a794f407b212588d0772b68dd9d [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
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800779 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800780 # Since we're bypassing github API, which would require a user, we
781 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800782 # insert the status at the top of the list, to simulate that it
783 # is the most recent set status
784 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100785 'state': state,
786 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800787 'description': description,
788 'context': context,
789 'creator': {
790 'login': user
791 }
792 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100793
Jesse Keatingae4cd272017-01-30 17:10:44 -0800794 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800795 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
796 # convert the timestamp to a str format that would be returned
797 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800798
Adam Gandelmand81dd762017-02-09 15:15:49 -0800799 if granted_on:
800 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
801 submitted_at = time.strftime(
802 gh_time_format, granted_on.timetuple())
803 else:
804 # github timestamps only down to the second, so we need to make
805 # sure reviews that tests add appear to be added over a period of
806 # time in the past and not all at once.
807 if not self.reviews:
808 # the first review happens 10 mins ago
809 offset = 600
810 else:
811 # subsequent reviews happen 1 minute closer to now
812 offset = 600 - (len(self.reviews) * 60)
813
814 granted_on = datetime.datetime.utcfromtimestamp(
815 time.time() - offset)
816 submitted_at = time.strftime(
817 gh_time_format, granted_on.timetuple())
818
Jesse Keatingae4cd272017-01-30 17:10:44 -0800819 self.reviews.append({
820 'state': state,
821 'user': {
822 'login': user,
823 'email': user + "@derp.com",
824 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800825 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800826 })
827
Gregory Haynes4fc12542015-04-22 20:38:06 -0700828 def _getPRReference(self):
829 return '%s/head' % self.number
830
831 def _getPullRequestEvent(self, action):
832 name = 'pull_request'
833 data = {
834 'action': action,
835 'number': self.number,
836 'pull_request': {
837 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100838 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 'updated_at': self.updated_at,
840 'base': {
841 'ref': self.branch,
842 'repo': {
843 'full_name': self.project
844 }
845 },
846 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800847 'sha': self.head_sha,
848 'repo': {
849 'full_name': self.project
850 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700851 }
Jan Hruban3b415922016-02-03 13:10:22 +0100852 },
853 'sender': {
854 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700855 }
856 }
857 return (name, data)
858
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800859 def getCommitStatusEvent(self, context, state='success', user='zuul'):
860 name = 'status'
861 data = {
862 'state': state,
863 'sha': self.head_sha,
864 'description': 'Test results for %s: %s' % (self.head_sha, state),
865 'target_url': 'http://zuul/%s' % self.head_sha,
866 'branches': [],
867 'context': context,
868 'sender': {
869 'login': user
870 }
871 }
872 return (name, data)
873
Gregory Haynes4fc12542015-04-22 20:38:06 -0700874
875class FakeGithubConnection(githubconnection.GithubConnection):
876 log = logging.getLogger("zuul.test.FakeGithubConnection")
877
878 def __init__(self, driver, connection_name, connection_config,
879 upstream_root=None):
880 super(FakeGithubConnection, self).__init__(driver, connection_name,
881 connection_config)
882 self.connection_name = connection_name
883 self.pr_number = 0
884 self.pull_requests = []
885 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100886 self.merge_failure = False
887 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888
Jan Hruban570d01c2016-03-10 21:51:32 +0100889 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700890 self.pr_number += 1
891 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100892 self, self.pr_number, project, branch, subject, self.upstream_root,
893 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700894 self.pull_requests.append(pull_request)
895 return pull_request
896
Jesse Keating71a47ff2017-06-06 11:36:43 -0700897 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
898 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700899 if not old_rev:
900 old_rev = '00000000000000000000000000000000'
901 if not new_rev:
902 new_rev = random_sha1()
903 name = 'push'
904 data = {
905 'ref': ref,
906 'before': old_rev,
907 'after': new_rev,
908 'repository': {
909 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700910 },
911 'commits': [
912 {
913 'added': added_files,
914 'removed': removed_files,
915 'modified': modified_files
916 }
917 ]
Wayne1a78c612015-06-11 17:14:13 -0700918 }
919 return (name, data)
920
Gregory Haynes4fc12542015-04-22 20:38:06 -0700921 def emitEvent(self, event):
922 """Emulates sending the GitHub webhook event to the connection."""
923 port = self.webapp.server.socket.getsockname()[1]
924 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700925 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926 headers = {'X-Github-Event': name}
927 req = urllib.request.Request(
928 'http://localhost:%s/connection/%s/payload'
929 % (port, self.connection_name),
930 data=payload, headers=headers)
931 urllib.request.urlopen(req)
932
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200933 def getPull(self, project, number):
934 pr = self.pull_requests[number - 1]
935 data = {
936 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100937 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200938 'updated_at': pr.updated_at,
939 'base': {
940 'repo': {
941 'full_name': pr.project
942 },
943 'ref': pr.branch,
944 },
Jan Hruban37615e52015-11-19 14:30:49 +0100945 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700946 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200947 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800948 'sha': pr.head_sha,
949 'repo': {
950 'full_name': pr.project
951 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200952 }
953 }
954 return data
955
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800956 def getPullBySha(self, sha):
957 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
958 if len(prs) > 1:
959 raise Exception('Multiple pulls found with head sha: %s' % sha)
960 pr = prs[0]
961 return self.getPull(pr.project, pr.number)
962
Jan Hruban570d01c2016-03-10 21:51:32 +0100963 def getPullFileNames(self, project, number):
964 pr = self.pull_requests[number - 1]
965 return pr.files
966
Jesse Keatingae4cd272017-01-30 17:10:44 -0800967 def _getPullReviews(self, owner, project, number):
968 pr = self.pull_requests[number - 1]
969 return pr.reviews
970
Jan Hruban3b415922016-02-03 13:10:22 +0100971 def getUser(self, login):
972 data = {
973 'username': login,
974 'name': 'Github User',
975 'email': 'github.user@example.com'
976 }
977 return data
978
Jesse Keatingae4cd272017-01-30 17:10:44 -0800979 def getRepoPermission(self, project, login):
980 owner, proj = project.split('/')
981 for pr in self.pull_requests:
982 pr_owner, pr_project = pr.project.split('/')
983 if (pr_owner == owner and proj == pr_project):
984 if login in pr.writers:
985 return 'write'
986 else:
987 return 'read'
988
Gregory Haynes4fc12542015-04-22 20:38:06 -0700989 def getGitUrl(self, project):
990 return os.path.join(self.upstream_root, str(project))
991
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200992 def real_getGitUrl(self, project):
993 return super(FakeGithubConnection, self).getGitUrl(project)
994
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995 def getProjectBranches(self, project):
996 """Masks getProjectBranches since we don't have a real github"""
997
998 # just returns master for now
999 return ['master']
1000
Jan Hrubane252a732017-01-03 15:03:09 +01001001 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -07001002 pull_request = self.pull_requests[pr_number - 1]
1003 pull_request.addComment(message)
1004
Jan Hruban3b415922016-02-03 13:10:22 +01001005 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +01001006 pull_request = self.pull_requests[pr_number - 1]
1007 if self.merge_failure:
1008 raise Exception('Pull request was not merged')
1009 if self.merge_not_allowed_count > 0:
1010 self.merge_not_allowed_count -= 1
1011 raise MergeFailure('Merge was not successful due to mergeability'
1012 ' conflict')
1013 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001014 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001015
Jesse Keatingd96e5882017-01-19 13:55:50 -08001016 def getCommitStatuses(self, project, sha):
1017 owner, proj = project.split('/')
1018 for pr in self.pull_requests:
1019 pr_owner, pr_project = pr.project.split('/')
Jesse Keating0d40c122017-05-26 11:32:53 -07001020 # This is somewhat risky, if the same commit exists in multiple
1021 # PRs, we might grab the wrong one that doesn't have a status
1022 # that is expected to be there. Maybe re-work this so that there
1023 # is a global registry of commit statuses like with github.
Jesse Keatingd96e5882017-01-19 13:55:50 -08001024 if (pr_owner == owner and pr_project == proj and
Jesse Keating0d40c122017-05-26 11:32:53 -07001025 sha in pr.statuses):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001026 return pr.statuses[sha]
1027
Jan Hrubane252a732017-01-03 15:03:09 +01001028 def setCommitStatus(self, project, sha, state,
1029 url='', description='', context=''):
1030 owner, proj = project.split('/')
1031 for pr in self.pull_requests:
1032 pr_owner, pr_project = pr.project.split('/')
1033 if (pr_owner == owner and pr_project == proj and
1034 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -08001035 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +01001036
Jan Hruban16ad31f2015-11-07 14:39:07 +01001037 def labelPull(self, project, pr_number, label):
1038 pull_request = self.pull_requests[pr_number - 1]
1039 pull_request.addLabel(label)
1040
1041 def unlabelPull(self, project, pr_number, label):
1042 pull_request = self.pull_requests[pr_number - 1]
1043 pull_request.removeLabel(label)
1044
Gregory Haynes4fc12542015-04-22 20:38:06 -07001045
Clark Boylanb640e052014-04-03 16:41:46 -07001046class BuildHistory(object):
1047 def __init__(self, **kw):
1048 self.__dict__.update(kw)
1049
1050 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001051 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1052 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001053
1054
Clark Boylanb640e052014-04-03 16:41:46 -07001055class FakeStatsd(threading.Thread):
1056 def __init__(self):
1057 threading.Thread.__init__(self)
1058 self.daemon = True
1059 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1060 self.sock.bind(('', 0))
1061 self.port = self.sock.getsockname()[1]
1062 self.wake_read, self.wake_write = os.pipe()
1063 self.stats = []
1064
1065 def run(self):
1066 while True:
1067 poll = select.poll()
1068 poll.register(self.sock, select.POLLIN)
1069 poll.register(self.wake_read, select.POLLIN)
1070 ret = poll.poll()
1071 for (fd, event) in ret:
1072 if fd == self.sock.fileno():
1073 data = self.sock.recvfrom(1024)
1074 if not data:
1075 return
1076 self.stats.append(data[0])
1077 if fd == self.wake_read:
1078 return
1079
1080 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001081 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001082
1083
James E. Blaire1767bc2016-08-02 10:00:27 -07001084class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001085 log = logging.getLogger("zuul.test")
1086
Paul Belanger174a8272017-03-14 13:20:10 -04001087 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001088 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001089 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001090 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001091 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001092 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001093 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001094 # TODOv3(jeblair): self.node is really "the label of the node
1095 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001096 # keep using it like this, or we may end up exposing more of
1097 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001098 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001099 self.node = None
1100 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001101 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001102 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001103 self.pipeline = self.parameters['ZUUL_PIPELINE']
1104 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001105 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001106 self.wait_condition = threading.Condition()
1107 self.waiting = False
1108 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001109 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001110 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001111 self.changes = None
1112 if 'ZUUL_CHANGE_IDS' in self.parameters:
1113 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001114
James E. Blair3158e282016-08-19 09:34:11 -07001115 def __repr__(self):
1116 waiting = ''
1117 if self.waiting:
1118 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001119 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1120 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001121
Clark Boylanb640e052014-04-03 16:41:46 -07001122 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001123 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001124 self.wait_condition.acquire()
1125 self.wait_condition.notify()
1126 self.waiting = False
1127 self.log.debug("Build %s released" % self.unique)
1128 self.wait_condition.release()
1129
1130 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001131 """Return whether this build is being held.
1132
1133 :returns: Whether the build is being held.
1134 :rtype: bool
1135 """
1136
Clark Boylanb640e052014-04-03 16:41:46 -07001137 self.wait_condition.acquire()
1138 if self.waiting:
1139 ret = True
1140 else:
1141 ret = False
1142 self.wait_condition.release()
1143 return ret
1144
1145 def _wait(self):
1146 self.wait_condition.acquire()
1147 self.waiting = True
1148 self.log.debug("Build %s waiting" % self.unique)
1149 self.wait_condition.wait()
1150 self.wait_condition.release()
1151
1152 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001153 self.log.debug('Running build %s' % self.unique)
1154
Paul Belanger174a8272017-03-14 13:20:10 -04001155 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001156 self.log.debug('Holding build %s' % self.unique)
1157 self._wait()
1158 self.log.debug("Build %s continuing" % self.unique)
1159
James E. Blair412fba82017-01-26 15:00:50 -08001160 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001161 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001162 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001163 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001164 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001165 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001166 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001167
James E. Blaire1767bc2016-08-02 10:00:27 -07001168 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001169
James E. Blaira5dba232016-08-08 15:53:24 -07001170 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001171 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001172 for change in changes:
1173 if self.hasChanges(change):
1174 return True
1175 return False
1176
James E. Blaire7b99a02016-08-05 14:27:34 -07001177 def hasChanges(self, *changes):
1178 """Return whether this build has certain changes in its git repos.
1179
1180 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001181 are expected to be present (in order) in the git repository of
1182 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001183
1184 :returns: Whether the build has the indicated changes.
1185 :rtype: bool
1186
1187 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001188 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001189 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001190 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001191 try:
1192 repo = git.Repo(path)
1193 except NoSuchPathError as e:
1194 self.log.debug('%s' % e)
1195 return False
1196 ref = self.parameters['ZUUL_REF']
1197 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1198 commit_message = '%s-1' % change.subject
1199 self.log.debug("Checking if build %s has changes; commit_message "
1200 "%s; repo_messages %s" % (self, commit_message,
1201 repo_messages))
1202 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001203 self.log.debug(" messages do not match")
1204 return False
1205 self.log.debug(" OK")
1206 return True
1207
James E. Blaird8af5422017-05-24 13:59:40 -07001208 def getWorkspaceRepos(self, projects):
1209 """Return workspace git repo objects for the listed projects
1210
1211 :arg list projects: A list of strings, each the canonical name
1212 of a project.
1213
1214 :returns: A dictionary of {name: repo} for every listed
1215 project.
1216 :rtype: dict
1217
1218 """
1219
1220 repos = {}
1221 for project in projects:
1222 path = os.path.join(self.jobdir.src_root, project)
1223 repo = git.Repo(path)
1224 repos[project] = repo
1225 return repos
1226
Clark Boylanb640e052014-04-03 16:41:46 -07001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1229 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001230
Paul Belanger174a8272017-03-14 13:20:10 -04001231 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001232 they will report that they have started but then pause until
1233 released before reporting completion. This attribute may be
1234 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001235 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001236 be explicitly released.
1237
1238 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001239 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001240 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001241 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001242 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001243 self.hold_jobs_in_build = False
1244 self.lock = threading.Lock()
1245 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001246 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001247 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001248 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001249
James E. Blaira5dba232016-08-08 15:53:24 -07001250 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001251 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001252
1253 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001254 :arg Change change: The :py:class:`~tests.base.FakeChange`
1255 instance which should cause the job to fail. This job
1256 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001257
1258 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001259 l = self.fail_tests.get(name, [])
1260 l.append(change)
1261 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001262
James E. Blair962220f2016-08-03 11:22:38 -07001263 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001264 """Release a held build.
1265
1266 :arg str regex: A regular expression which, if supplied, will
1267 cause only builds with matching names to be released. If
1268 not supplied, all builds will be released.
1269
1270 """
James E. Blair962220f2016-08-03 11:22:38 -07001271 builds = self.running_builds[:]
1272 self.log.debug("Releasing build %s (%s)" % (regex,
1273 len(self.running_builds)))
1274 for build in builds:
1275 if not regex or re.match(regex, build.name):
1276 self.log.debug("Releasing build %s" %
1277 (build.parameters['ZUUL_UUID']))
1278 build.release()
1279 else:
1280 self.log.debug("Not releasing build %s" %
1281 (build.parameters['ZUUL_UUID']))
1282 self.log.debug("Done releasing builds %s (%s)" %
1283 (regex, len(self.running_builds)))
1284
Paul Belanger174a8272017-03-14 13:20:10 -04001285 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001286 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001287 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001288 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001289 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001290 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001291 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001292 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001293 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1294 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001295
1296 def stopJob(self, job):
1297 self.log.debug("handle stop")
1298 parameters = json.loads(job.arguments)
1299 uuid = parameters['uuid']
1300 for build in self.running_builds:
1301 if build.unique == uuid:
1302 build.aborted = True
1303 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001304 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001305
James E. Blaira002b032017-04-18 10:35:48 -07001306 def stop(self):
1307 for build in self.running_builds:
1308 build.release()
1309 super(RecordingExecutorServer, self).stop()
1310
Joshua Hesketh50c21782016-10-13 21:34:14 +11001311
Paul Belanger174a8272017-03-14 13:20:10 -04001312class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001313 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001314 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001315 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001316 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001317 if not commit: # merge conflict
1318 self.recordResult('MERGER_FAILURE')
1319 return commit
1320
1321 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001322 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001323 self.executor_server.lock.acquire()
1324 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001325 BuildHistory(name=build.name, result=result, changes=build.changes,
1326 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001327 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001328 pipeline=build.parameters['ZUUL_PIPELINE'])
1329 )
Paul Belanger174a8272017-03-14 13:20:10 -04001330 self.executor_server.running_builds.remove(build)
1331 del self.executor_server.job_builds[self.job.unique]
1332 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001333
1334 def runPlaybooks(self, args):
1335 build = self.executor_server.job_builds[self.job.unique]
1336 build.jobdir = self.jobdir
1337
1338 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1339 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001340 return result
1341
Monty Taylore6562aa2017-02-20 07:37:39 -05001342 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001343 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001344
Paul Belanger174a8272017-03-14 13:20:10 -04001345 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001346 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001347 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001348 else:
1349 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001350 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001351
James E. Blairad8dca02017-02-21 11:48:32 -05001352 def getHostList(self, args):
1353 self.log.debug("hostlist")
1354 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001355 for host in hosts:
1356 host['host_vars']['ansible_connection'] = 'local'
1357
1358 hosts.append(dict(
1359 name='localhost',
1360 host_vars=dict(ansible_connection='local'),
1361 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001362 return hosts
1363
James E. Blairf5dbd002015-12-23 15:26:17 -08001364
Clark Boylanb640e052014-04-03 16:41:46 -07001365class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001366 """A Gearman server for use in tests.
1367
1368 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1369 added to the queue but will not be distributed to workers
1370 until released. This attribute may be changed at any time and
1371 will take effect for subsequently enqueued jobs, but
1372 previously held jobs will still need to be explicitly
1373 released.
1374
1375 """
1376
Clark Boylanb640e052014-04-03 16:41:46 -07001377 def __init__(self):
1378 self.hold_jobs_in_queue = False
1379 super(FakeGearmanServer, self).__init__(0)
1380
1381 def getJobForConnection(self, connection, peek=False):
1382 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1383 for job in queue:
1384 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001385 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001386 job.waiting = self.hold_jobs_in_queue
1387 else:
1388 job.waiting = False
1389 if job.waiting:
1390 continue
1391 if job.name in connection.functions:
1392 if not peek:
1393 queue.remove(job)
1394 connection.related_jobs[job.handle] = job
1395 job.worker_connection = connection
1396 job.running = True
1397 return job
1398 return None
1399
1400 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001401 """Release a held job.
1402
1403 :arg str regex: A regular expression which, if supplied, will
1404 cause only jobs with matching names to be released. If
1405 not supplied, all jobs will be released.
1406 """
Clark Boylanb640e052014-04-03 16:41:46 -07001407 released = False
1408 qlen = (len(self.high_queue) + len(self.normal_queue) +
1409 len(self.low_queue))
1410 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1411 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001412 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001413 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001414 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001415 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001416 self.log.debug("releasing queued job %s" %
1417 job.unique)
1418 job.waiting = False
1419 released = True
1420 else:
1421 self.log.debug("not releasing queued job %s" %
1422 job.unique)
1423 if released:
1424 self.wakeConnections()
1425 qlen = (len(self.high_queue) + len(self.normal_queue) +
1426 len(self.low_queue))
1427 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1428
1429
1430class FakeSMTP(object):
1431 log = logging.getLogger('zuul.FakeSMTP')
1432
1433 def __init__(self, messages, server, port):
1434 self.server = server
1435 self.port = port
1436 self.messages = messages
1437
1438 def sendmail(self, from_email, to_email, msg):
1439 self.log.info("Sending email from %s, to %s, with msg %s" % (
1440 from_email, to_email, msg))
1441
1442 headers = msg.split('\n\n', 1)[0]
1443 body = msg.split('\n\n', 1)[1]
1444
1445 self.messages.append(dict(
1446 from_email=from_email,
1447 to_email=to_email,
1448 msg=msg,
1449 headers=headers,
1450 body=body,
1451 ))
1452
1453 return True
1454
1455 def quit(self):
1456 return True
1457
1458
James E. Blairdce6cea2016-12-20 16:45:32 -08001459class FakeNodepool(object):
1460 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001461 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001462
1463 log = logging.getLogger("zuul.test.FakeNodepool")
1464
1465 def __init__(self, host, port, chroot):
1466 self.client = kazoo.client.KazooClient(
1467 hosts='%s:%s%s' % (host, port, chroot))
1468 self.client.start()
1469 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001470 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001471 self.thread = threading.Thread(target=self.run)
1472 self.thread.daemon = True
1473 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001474 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001475
1476 def stop(self):
1477 self._running = False
1478 self.thread.join()
1479 self.client.stop()
1480 self.client.close()
1481
1482 def run(self):
1483 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001484 try:
1485 self._run()
1486 except Exception:
1487 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001488 time.sleep(0.1)
1489
1490 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001491 if self.paused:
1492 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001493 for req in self.getNodeRequests():
1494 self.fulfillRequest(req)
1495
1496 def getNodeRequests(self):
1497 try:
1498 reqids = self.client.get_children(self.REQUEST_ROOT)
1499 except kazoo.exceptions.NoNodeError:
1500 return []
1501 reqs = []
1502 for oid in sorted(reqids):
1503 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001504 try:
1505 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001506 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001507 data['_oid'] = oid
1508 reqs.append(data)
1509 except kazoo.exceptions.NoNodeError:
1510 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001511 return reqs
1512
James E. Blaire18d4602017-01-05 11:17:28 -08001513 def getNodes(self):
1514 try:
1515 nodeids = self.client.get_children(self.NODE_ROOT)
1516 except kazoo.exceptions.NoNodeError:
1517 return []
1518 nodes = []
1519 for oid in sorted(nodeids):
1520 path = self.NODE_ROOT + '/' + oid
1521 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001522 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001523 data['_oid'] = oid
1524 try:
1525 lockfiles = self.client.get_children(path + '/lock')
1526 except kazoo.exceptions.NoNodeError:
1527 lockfiles = []
1528 if lockfiles:
1529 data['_lock'] = True
1530 else:
1531 data['_lock'] = False
1532 nodes.append(data)
1533 return nodes
1534
James E. Blaira38c28e2017-01-04 10:33:20 -08001535 def makeNode(self, request_id, node_type):
1536 now = time.time()
1537 path = '/nodepool/nodes/'
1538 data = dict(type=node_type,
1539 provider='test-provider',
1540 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001541 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001542 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001543 public_ipv4='127.0.0.1',
1544 private_ipv4=None,
1545 public_ipv6=None,
1546 allocated_to=request_id,
1547 state='ready',
1548 state_time=now,
1549 created_time=now,
1550 updated_time=now,
1551 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001552 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001553 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001554 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001555 path = self.client.create(path, data,
1556 makepath=True,
1557 sequence=True)
1558 nodeid = path.split("/")[-1]
1559 return nodeid
1560
James E. Blair6ab79e02017-01-06 10:10:17 -08001561 def addFailRequest(self, request):
1562 self.fail_requests.add(request['_oid'])
1563
James E. Blairdce6cea2016-12-20 16:45:32 -08001564 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001565 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001566 return
1567 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001568 oid = request['_oid']
1569 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001570
James E. Blair6ab79e02017-01-06 10:10:17 -08001571 if oid in self.fail_requests:
1572 request['state'] = 'failed'
1573 else:
1574 request['state'] = 'fulfilled'
1575 nodes = []
1576 for node in request['node_types']:
1577 nodeid = self.makeNode(oid, node)
1578 nodes.append(nodeid)
1579 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001580
James E. Blaira38c28e2017-01-04 10:33:20 -08001581 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001582 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001583 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001584 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001585 try:
1586 self.client.set(path, data)
1587 except kazoo.exceptions.NoNodeError:
1588 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001589
1590
James E. Blair498059b2016-12-20 13:50:13 -08001591class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001592 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001593 super(ChrootedKazooFixture, self).__init__()
1594
1595 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1596 if ':' in zk_host:
1597 host, port = zk_host.split(':')
1598 else:
1599 host = zk_host
1600 port = None
1601
1602 self.zookeeper_host = host
1603
1604 if not port:
1605 self.zookeeper_port = 2181
1606 else:
1607 self.zookeeper_port = int(port)
1608
Clark Boylan621ec9a2017-04-07 17:41:33 -07001609 self.test_id = test_id
1610
James E. Blair498059b2016-12-20 13:50:13 -08001611 def _setUp(self):
1612 # Make sure the test chroot paths do not conflict
1613 random_bits = ''.join(random.choice(string.ascii_lowercase +
1614 string.ascii_uppercase)
1615 for x in range(8))
1616
Clark Boylan621ec9a2017-04-07 17:41:33 -07001617 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001618 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1619
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001620 self.addCleanup(self._cleanup)
1621
James E. Blair498059b2016-12-20 13:50:13 -08001622 # Ensure the chroot path exists and clean up any pre-existing znodes.
1623 _tmp_client = kazoo.client.KazooClient(
1624 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1625 _tmp_client.start()
1626
1627 if _tmp_client.exists(self.zookeeper_chroot):
1628 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1629
1630 _tmp_client.ensure_path(self.zookeeper_chroot)
1631 _tmp_client.stop()
1632 _tmp_client.close()
1633
James E. Blair498059b2016-12-20 13:50:13 -08001634 def _cleanup(self):
1635 '''Remove the chroot path.'''
1636 # Need a non-chroot'ed client to remove the chroot path
1637 _tmp_client = kazoo.client.KazooClient(
1638 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1639 _tmp_client.start()
1640 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1641 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001642 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001643
1644
Joshua Heskethd78b4482015-09-14 16:56:34 -06001645class MySQLSchemaFixture(fixtures.Fixture):
1646 def setUp(self):
1647 super(MySQLSchemaFixture, self).setUp()
1648
1649 random_bits = ''.join(random.choice(string.ascii_lowercase +
1650 string.ascii_uppercase)
1651 for x in range(8))
1652 self.name = '%s_%s' % (random_bits, os.getpid())
1653 self.passwd = uuid.uuid4().hex
1654 db = pymysql.connect(host="localhost",
1655 user="openstack_citest",
1656 passwd="openstack_citest",
1657 db="openstack_citest")
1658 cur = db.cursor()
1659 cur.execute("create database %s" % self.name)
1660 cur.execute(
1661 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1662 (self.name, self.name, self.passwd))
1663 cur.execute("flush privileges")
1664
1665 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1666 self.passwd,
1667 self.name)
1668 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1669 self.addCleanup(self.cleanup)
1670
1671 def cleanup(self):
1672 db = pymysql.connect(host="localhost",
1673 user="openstack_citest",
1674 passwd="openstack_citest",
1675 db="openstack_citest")
1676 cur = db.cursor()
1677 cur.execute("drop database %s" % self.name)
1678 cur.execute("drop user '%s'@'localhost'" % self.name)
1679 cur.execute("flush privileges")
1680
1681
Maru Newby3fe5f852015-01-13 04:22:14 +00001682class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001683 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001684 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001685
James E. Blair1c236df2017-02-01 14:07:24 -08001686 def attachLogs(self, *args):
1687 def reader():
1688 self._log_stream.seek(0)
1689 while True:
1690 x = self._log_stream.read(4096)
1691 if not x:
1692 break
1693 yield x.encode('utf8')
1694 content = testtools.content.content_from_reader(
1695 reader,
1696 testtools.content_type.UTF8_TEXT,
1697 False)
1698 self.addDetail('logging', content)
1699
Clark Boylanb640e052014-04-03 16:41:46 -07001700 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001701 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001702 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1703 try:
1704 test_timeout = int(test_timeout)
1705 except ValueError:
1706 # If timeout value is invalid do not set a timeout.
1707 test_timeout = 0
1708 if test_timeout > 0:
1709 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1710
1711 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1712 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1713 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1714 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1715 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1716 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1717 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1718 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1719 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1720 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001721 self._log_stream = StringIO()
1722 self.addOnException(self.attachLogs)
1723 else:
1724 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001725
James E. Blair73b41772017-05-22 13:22:55 -07001726 # NOTE(jeblair): this is temporary extra debugging to try to
1727 # track down a possible leak.
1728 orig_git_repo_init = git.Repo.__init__
1729
1730 def git_repo_init(myself, *args, **kw):
1731 orig_git_repo_init(myself, *args, **kw)
1732 self.log.debug("Created git repo 0x%x %s" %
1733 (id(myself), repr(myself)))
1734
1735 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1736 git_repo_init))
1737
James E. Blair1c236df2017-02-01 14:07:24 -08001738 handler = logging.StreamHandler(self._log_stream)
1739 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1740 '%(levelname)-8s %(message)s')
1741 handler.setFormatter(formatter)
1742
1743 logger = logging.getLogger()
1744 logger.setLevel(logging.DEBUG)
1745 logger.addHandler(handler)
1746
Clark Boylan3410d532017-04-25 12:35:29 -07001747 # Make sure we don't carry old handlers around in process state
1748 # which slows down test runs
1749 self.addCleanup(logger.removeHandler, handler)
1750 self.addCleanup(handler.close)
1751 self.addCleanup(handler.flush)
1752
James E. Blair1c236df2017-02-01 14:07:24 -08001753 # NOTE(notmorgan): Extract logging overrides for specific
1754 # libraries from the OS_LOG_DEFAULTS env and create loggers
1755 # for each. This is used to limit the output during test runs
1756 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001757 log_defaults_from_env = os.environ.get(
1758 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001759 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001760
James E. Blairdce6cea2016-12-20 16:45:32 -08001761 if log_defaults_from_env:
1762 for default in log_defaults_from_env.split(','):
1763 try:
1764 name, level_str = default.split('=', 1)
1765 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001766 logger = logging.getLogger(name)
1767 logger.setLevel(level)
1768 logger.addHandler(handler)
1769 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001770 except ValueError:
1771 # NOTE(notmorgan): Invalid format of the log default,
1772 # skip and don't try and apply a logger for the
1773 # specified module
1774 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001775
Maru Newby3fe5f852015-01-13 04:22:14 +00001776
1777class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001778 """A test case with a functioning Zuul.
1779
1780 The following class variables are used during test setup and can
1781 be overidden by subclasses but are effectively read-only once a
1782 test method starts running:
1783
1784 :cvar str config_file: This points to the main zuul config file
1785 within the fixtures directory. Subclasses may override this
1786 to obtain a different behavior.
1787
1788 :cvar str tenant_config_file: This is the tenant config file
1789 (which specifies from what git repos the configuration should
1790 be loaded). It defaults to the value specified in
1791 `config_file` but can be overidden by subclasses to obtain a
1792 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001793 configuration. See also the :py:func:`simple_layout`
1794 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001795
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001796 :cvar bool create_project_keys: Indicates whether Zuul should
1797 auto-generate keys for each project, or whether the test
1798 infrastructure should insert dummy keys to save time during
1799 startup. Defaults to False.
1800
James E. Blaire7b99a02016-08-05 14:27:34 -07001801 The following are instance variables that are useful within test
1802 methods:
1803
1804 :ivar FakeGerritConnection fake_<connection>:
1805 A :py:class:`~tests.base.FakeGerritConnection` will be
1806 instantiated for each connection present in the config file
1807 and stored here. For instance, `fake_gerrit` will hold the
1808 FakeGerritConnection object for a connection named `gerrit`.
1809
1810 :ivar FakeGearmanServer gearman_server: An instance of
1811 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1812 server that all of the Zuul components in this test use to
1813 communicate with each other.
1814
Paul Belanger174a8272017-03-14 13:20:10 -04001815 :ivar RecordingExecutorServer executor_server: An instance of
1816 :py:class:`~tests.base.RecordingExecutorServer` which is the
1817 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001818
1819 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1820 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001821 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001822 list upon completion.
1823
1824 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1825 objects representing completed builds. They are appended to
1826 the list in the order they complete.
1827
1828 """
1829
James E. Blair83005782015-12-11 14:46:03 -08001830 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001831 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001832 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001833
1834 def _startMerger(self):
1835 self.merge_server = zuul.merger.server.MergeServer(self.config,
1836 self.connections)
1837 self.merge_server.start()
1838
Maru Newby3fe5f852015-01-13 04:22:14 +00001839 def setUp(self):
1840 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001841
1842 self.setupZK()
1843
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001844 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001845 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001846 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1847 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001848 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001849 tmp_root = tempfile.mkdtemp(
1850 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001851 self.test_root = os.path.join(tmp_root, "zuul-test")
1852 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001853 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001854 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001855 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001856
1857 if os.path.exists(self.test_root):
1858 shutil.rmtree(self.test_root)
1859 os.makedirs(self.test_root)
1860 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001861 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001862
1863 # Make per test copy of Configuration.
1864 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001865 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1866 if not os.path.exists(self.private_key_file):
1867 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1868 shutil.copy(src_private_key_file, self.private_key_file)
1869 shutil.copy('{}.pub'.format(src_private_key_file),
1870 '{}.pub'.format(self.private_key_file))
1871 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001872 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001873 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001874 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001875 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001876 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001877 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001878 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001879
Clark Boylanb640e052014-04-03 16:41:46 -07001880 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001881 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1882 # see: https://github.com/jsocol/pystatsd/issues/61
1883 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001884 os.environ['STATSD_PORT'] = str(self.statsd.port)
1885 self.statsd.start()
1886 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001887 reload_module(statsd)
1888 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001889
1890 self.gearman_server = FakeGearmanServer()
1891
1892 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001893 self.log.info("Gearman server on port %s" %
1894 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001895
James E. Blaire511d2f2016-12-08 15:22:26 -08001896 gerritsource.GerritSource.replication_timeout = 1.5
1897 gerritsource.GerritSource.replication_retry_interval = 0.5
1898 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001899
Joshua Hesketh352264b2015-08-11 23:42:08 +10001900 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001901
Jan Hruban7083edd2015-08-21 14:00:54 +02001902 self.webapp = zuul.webapp.WebApp(
1903 self.sched, port=0, listen_address='127.0.0.1')
1904
Jan Hruban6b71aff2015-10-22 16:58:08 +02001905 self.event_queues = [
1906 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001907 self.sched.trigger_event_queue,
1908 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001909 ]
1910
James E. Blairfef78942016-03-11 16:28:56 -08001911 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001912 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001913
Paul Belanger174a8272017-03-14 13:20:10 -04001914 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001915 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001916 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001917 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001918 _test_root=self.test_root,
1919 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001920 self.executor_server.start()
1921 self.history = self.executor_server.build_history
1922 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001923
Paul Belanger174a8272017-03-14 13:20:10 -04001924 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001925 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001926 self.merge_client = zuul.merger.client.MergeClient(
1927 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001928 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001929 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001930 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001931
James E. Blair0d5a36e2017-02-21 10:53:44 -05001932 self.fake_nodepool = FakeNodepool(
1933 self.zk_chroot_fixture.zookeeper_host,
1934 self.zk_chroot_fixture.zookeeper_port,
1935 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001936
Paul Belanger174a8272017-03-14 13:20:10 -04001937 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001938 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001939 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001940 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001941
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001942 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001943
1944 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001945 self.webapp.start()
1946 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001947 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001948 # Cleanups are run in reverse order
1949 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001950 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001951 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001952
James E. Blairb9c0d772017-03-03 14:34:49 -08001953 self.sched.reconfigure(self.config)
1954 self.sched.resume()
1955
Tobias Henkel7df274b2017-05-26 17:41:11 +02001956 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001957 # Set up gerrit related fakes
1958 # Set a changes database so multiple FakeGerrit's can report back to
1959 # a virtual canonical database given by the configured hostname
1960 self.gerrit_changes_dbs = {}
1961
1962 def getGerritConnection(driver, name, config):
1963 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1964 con = FakeGerritConnection(driver, name, config,
1965 changes_db=db,
1966 upstream_root=self.upstream_root)
1967 self.event_queues.append(con.event_queue)
1968 setattr(self, 'fake_' + name, con)
1969 return con
1970
1971 self.useFixture(fixtures.MonkeyPatch(
1972 'zuul.driver.gerrit.GerritDriver.getConnection',
1973 getGerritConnection))
1974
Gregory Haynes4fc12542015-04-22 20:38:06 -07001975 def getGithubConnection(driver, name, config):
1976 con = FakeGithubConnection(driver, name, config,
1977 upstream_root=self.upstream_root)
1978 setattr(self, 'fake_' + name, con)
1979 return con
1980
1981 self.useFixture(fixtures.MonkeyPatch(
1982 'zuul.driver.github.GithubDriver.getConnection',
1983 getGithubConnection))
1984
James E. Blaire511d2f2016-12-08 15:22:26 -08001985 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001986 # TODO(jhesketh): This should come from lib.connections for better
1987 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001988 # Register connections from the config
1989 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001990
Joshua Hesketh352264b2015-08-11 23:42:08 +10001991 def FakeSMTPFactory(*args, **kw):
1992 args = [self.smtp_messages] + list(args)
1993 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001994
Joshua Hesketh352264b2015-08-11 23:42:08 +10001995 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001996
James E. Blaire511d2f2016-12-08 15:22:26 -08001997 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001998 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02001999 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002000
James E. Blair83005782015-12-11 14:46:03 -08002001 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002002 # This creates the per-test configuration object. It can be
2003 # overriden by subclasses, but should not need to be since it
2004 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07002005 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002006 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002007
2008 if not self.setupSimpleLayout():
2009 if hasattr(self, 'tenant_config_file'):
2010 self.config.set('zuul', 'tenant_config',
2011 self.tenant_config_file)
2012 git_path = os.path.join(
2013 os.path.dirname(
2014 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2015 'git')
2016 if os.path.exists(git_path):
2017 for reponame in os.listdir(git_path):
2018 project = reponame.replace('_', '/')
2019 self.copyDirToRepo(project,
2020 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002021 self.setupAllProjectKeys()
2022
James E. Blair06cc3922017-04-19 10:08:10 -07002023 def setupSimpleLayout(self):
2024 # If the test method has been decorated with a simple_layout,
2025 # use that instead of the class tenant_config_file. Set up a
2026 # single config-project with the specified layout, and
2027 # initialize repos for all of the 'project' entries which
2028 # appear in the layout.
2029 test_name = self.id().split('.')[-1]
2030 test = getattr(self, test_name)
2031 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002032 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002033 else:
2034 return False
2035
James E. Blairb70e55a2017-04-19 12:57:02 -07002036 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002037 path = os.path.join(FIXTURE_DIR, path)
2038 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002039 data = f.read()
2040 layout = yaml.safe_load(data)
2041 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002042 untrusted_projects = []
2043 for item in layout:
2044 if 'project' in item:
2045 name = item['project']['name']
2046 untrusted_projects.append(name)
2047 self.init_repo(name)
2048 self.addCommitToRepo(name, 'initial commit',
2049 files={'README': ''},
2050 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002051 if 'job' in item:
2052 jobname = item['job']['name']
2053 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002054
2055 root = os.path.join(self.test_root, "config")
2056 if not os.path.exists(root):
2057 os.makedirs(root)
2058 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2059 config = [{'tenant':
2060 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002061 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002062 {'config-projects': ['common-config'],
2063 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002064 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002065 f.close()
2066 self.config.set('zuul', 'tenant_config',
2067 os.path.join(FIXTURE_DIR, f.name))
2068
2069 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002070 self.addCommitToRepo('common-config', 'add content from fixture',
2071 files, branch='master', tag='init')
2072
2073 return True
2074
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002075 def setupAllProjectKeys(self):
2076 if self.create_project_keys:
2077 return
2078
2079 path = self.config.get('zuul', 'tenant_config')
2080 with open(os.path.join(FIXTURE_DIR, path)) as f:
2081 tenant_config = yaml.safe_load(f.read())
2082 for tenant in tenant_config:
2083 sources = tenant['tenant']['source']
2084 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002085 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002086 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002087 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002088 self.setupProjectKeys(source, project)
2089
2090 def setupProjectKeys(self, source, project):
2091 # Make sure we set up an RSA key for the project so that we
2092 # don't spend time generating one:
2093
2094 key_root = os.path.join(self.state_root, 'keys')
2095 if not os.path.isdir(key_root):
2096 os.mkdir(key_root, 0o700)
2097 private_key_file = os.path.join(key_root, source, project + '.pem')
2098 private_key_dir = os.path.dirname(private_key_file)
2099 self.log.debug("Installing test keys for project %s at %s" % (
2100 project, private_key_file))
2101 if not os.path.isdir(private_key_dir):
2102 os.makedirs(private_key_dir)
2103 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2104 with open(private_key_file, 'w') as o:
2105 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002106
James E. Blair498059b2016-12-20 13:50:13 -08002107 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002108 self.zk_chroot_fixture = self.useFixture(
2109 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002110 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002111 self.zk_chroot_fixture.zookeeper_host,
2112 self.zk_chroot_fixture.zookeeper_port,
2113 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002114
James E. Blair96c6bf82016-01-15 16:20:40 -08002115 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002116 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002117
2118 files = {}
2119 for (dirpath, dirnames, filenames) in os.walk(source_path):
2120 for filename in filenames:
2121 test_tree_filepath = os.path.join(dirpath, filename)
2122 common_path = os.path.commonprefix([test_tree_filepath,
2123 source_path])
2124 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2125 with open(test_tree_filepath, 'r') as f:
2126 content = f.read()
2127 files[relative_filepath] = content
2128 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002129 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002130
James E. Blaire18d4602017-01-05 11:17:28 -08002131 def assertNodepoolState(self):
2132 # Make sure that there are no pending requests
2133
2134 requests = self.fake_nodepool.getNodeRequests()
2135 self.assertEqual(len(requests), 0)
2136
2137 nodes = self.fake_nodepool.getNodes()
2138 for node in nodes:
2139 self.assertFalse(node['_lock'], "Node %s is locked" %
2140 (node['_oid'],))
2141
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002142 def assertNoGeneratedKeys(self):
2143 # Make sure that Zuul did not generate any project keys
2144 # (unless it was supposed to).
2145
2146 if self.create_project_keys:
2147 return
2148
2149 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2150 test_key = i.read()
2151
2152 key_root = os.path.join(self.state_root, 'keys')
2153 for root, dirname, files in os.walk(key_root):
2154 for fn in files:
2155 with open(os.path.join(root, fn)) as f:
2156 self.assertEqual(test_key, f.read())
2157
Clark Boylanb640e052014-04-03 16:41:46 -07002158 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002159 self.log.debug("Assert final state")
2160 # Make sure no jobs are running
2161 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002162 # Make sure that git.Repo objects have been garbage collected.
2163 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002164 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002165 gc.collect()
2166 for obj in gc.get_objects():
2167 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002168 self.log.debug("Leaked git repo object: 0x%x %s" %
2169 (id(obj), repr(obj)))
2170 for ref in gc.get_referrers(obj):
2171 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002172 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002173 if repos:
2174 for obj in gc.garbage:
2175 self.log.debug(" Garbage %s" % (repr(obj)))
2176 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002177 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002178 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002179 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002180 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002181 for tenant in self.sched.abide.tenants.values():
2182 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002183 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002184 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002185
2186 def shutdown(self):
2187 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002188 self.executor_server.hold_jobs_in_build = False
2189 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002190 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002191 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002192 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002193 self.sched.stop()
2194 self.sched.join()
2195 self.statsd.stop()
2196 self.statsd.join()
2197 self.webapp.stop()
2198 self.webapp.join()
2199 self.rpc.stop()
2200 self.rpc.join()
2201 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002202 self.fake_nodepool.stop()
2203 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002204 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002205 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002206 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002207 # Further the pydevd threads also need to be whitelisted so debugging
2208 # e.g. in PyCharm is possible without breaking shutdown.
2209 whitelist = ['executor-watchdog',
2210 'pydevd.CommandThread',
2211 'pydevd.Reader',
2212 'pydevd.Writer',
2213 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002214 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002215 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002216 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002217 log_str = ""
2218 for thread_id, stack_frame in sys._current_frames().items():
2219 log_str += "Thread: %s\n" % thread_id
2220 log_str += "".join(traceback.format_stack(stack_frame))
2221 self.log.debug(log_str)
2222 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002223
James E. Blaira002b032017-04-18 10:35:48 -07002224 def assertCleanShutdown(self):
2225 pass
2226
James E. Blairc4ba97a2017-04-19 16:26:24 -07002227 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002228 parts = project.split('/')
2229 path = os.path.join(self.upstream_root, *parts[:-1])
2230 if not os.path.exists(path):
2231 os.makedirs(path)
2232 path = os.path.join(self.upstream_root, project)
2233 repo = git.Repo.init(path)
2234
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002235 with repo.config_writer() as config_writer:
2236 config_writer.set_value('user', 'email', 'user@example.com')
2237 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002238
Clark Boylanb640e052014-04-03 16:41:46 -07002239 repo.index.commit('initial commit')
2240 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002241 if tag:
2242 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002243
James E. Blair97d902e2014-08-21 13:25:56 -07002244 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002245 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002246 repo.git.clean('-x', '-f', '-d')
2247
James E. Blair97d902e2014-08-21 13:25:56 -07002248 def create_branch(self, project, branch):
2249 path = os.path.join(self.upstream_root, project)
2250 repo = git.Repo.init(path)
2251 fn = os.path.join(path, 'README')
2252
2253 branch_head = repo.create_head(branch)
2254 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002255 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002256 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002257 f.close()
2258 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002259 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002260
James E. Blair97d902e2014-08-21 13:25:56 -07002261 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002262 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002263 repo.git.clean('-x', '-f', '-d')
2264
Sachi King9f16d522016-03-16 12:20:45 +11002265 def create_commit(self, project):
2266 path = os.path.join(self.upstream_root, project)
2267 repo = git.Repo(path)
2268 repo.head.reference = repo.heads['master']
2269 file_name = os.path.join(path, 'README')
2270 with open(file_name, 'a') as f:
2271 f.write('creating fake commit\n')
2272 repo.index.add([file_name])
2273 commit = repo.index.commit('Creating a fake commit')
2274 return commit.hexsha
2275
James E. Blairf4a5f022017-04-18 14:01:10 -07002276 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002277 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002278 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002279 while len(self.builds):
2280 self.release(self.builds[0])
2281 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002282 i += 1
2283 if count is not None and i >= count:
2284 break
James E. Blairb8c16472015-05-05 14:55:26 -07002285
Clark Boylanb640e052014-04-03 16:41:46 -07002286 def release(self, job):
2287 if isinstance(job, FakeBuild):
2288 job.release()
2289 else:
2290 job.waiting = False
2291 self.log.debug("Queued job %s released" % job.unique)
2292 self.gearman_server.wakeConnections()
2293
2294 def getParameter(self, job, name):
2295 if isinstance(job, FakeBuild):
2296 return job.parameters[name]
2297 else:
2298 parameters = json.loads(job.arguments)
2299 return parameters[name]
2300
Clark Boylanb640e052014-04-03 16:41:46 -07002301 def haveAllBuildsReported(self):
2302 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002303 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002304 return False
2305 # Find out if every build that the worker has completed has been
2306 # reported back to Zuul. If it hasn't then that means a Gearman
2307 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002308 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002309 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002310 if not zbuild:
2311 # It has already been reported
2312 continue
2313 # It hasn't been reported yet.
2314 return False
2315 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002316 worker = self.executor_server.executor_worker
2317 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002318 if connection.state == 'GRAB_WAIT':
2319 return False
2320 return True
2321
2322 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002323 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002324 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002325 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002326 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002327 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002328 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002329 for j in conn.related_jobs.values():
2330 if j.unique == build.uuid:
2331 client_job = j
2332 break
2333 if not client_job:
2334 self.log.debug("%s is not known to the gearman client" %
2335 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002336 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002337 if not client_job.handle:
2338 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002339 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002340 server_job = self.gearman_server.jobs.get(client_job.handle)
2341 if not server_job:
2342 self.log.debug("%s is not known to the gearman server" %
2343 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002344 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002345 if not hasattr(server_job, 'waiting'):
2346 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002348 if server_job.waiting:
2349 continue
James E. Blair17302972016-08-10 16:11:42 -07002350 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002351 self.log.debug("%s has not reported start" % build)
2352 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002353 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002354 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002355 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002356 if worker_build:
2357 if worker_build.isWaiting():
2358 continue
2359 else:
2360 self.log.debug("%s is running" % worker_build)
2361 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002362 else:
James E. Blair962220f2016-08-03 11:22:38 -07002363 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002364 return False
James E. Blaira002b032017-04-18 10:35:48 -07002365 for (build_uuid, job_worker) in \
2366 self.executor_server.job_workers.items():
2367 if build_uuid not in seen_builds:
2368 self.log.debug("%s is not finalized" % build_uuid)
2369 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002370 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002371
James E. Blairdce6cea2016-12-20 16:45:32 -08002372 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002373 if self.fake_nodepool.paused:
2374 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002375 if self.sched.nodepool.requests:
2376 return False
2377 return True
2378
Jan Hruban6b71aff2015-10-22 16:58:08 +02002379 def eventQueuesEmpty(self):
2380 for queue in self.event_queues:
2381 yield queue.empty()
2382
2383 def eventQueuesJoin(self):
2384 for queue in self.event_queues:
2385 queue.join()
2386
Clark Boylanb640e052014-04-03 16:41:46 -07002387 def waitUntilSettled(self):
2388 self.log.debug("Waiting until settled...")
2389 start = time.time()
2390 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002391 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002392 self.log.error("Timeout waiting for Zuul to settle")
2393 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002394 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002395 self.log.error(" %s: %s" % (queue, queue.empty()))
2396 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002397 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002398 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002399 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002400 self.log.error("All requests completed: %s" %
2401 (self.areAllNodeRequestsComplete(),))
2402 self.log.error("Merge client jobs: %s" %
2403 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002404 raise Exception("Timeout waiting for Zuul to settle")
2405 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002406
Paul Belanger174a8272017-03-14 13:20:10 -04002407 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002408 # have all build states propogated to zuul?
2409 if self.haveAllBuildsReported():
2410 # Join ensures that the queue is empty _and_ events have been
2411 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002412 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002413 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002414 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002415 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002416 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002417 self.areAllNodeRequestsComplete() and
2418 all(self.eventQueuesEmpty())):
2419 # The queue empty check is placed at the end to
2420 # ensure that if a component adds an event between
2421 # when locked the run handler and checked that the
2422 # components were stable, we don't erroneously
2423 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002424 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002425 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002426 self.log.debug("...settled.")
2427 return
2428 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002429 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002430 self.sched.wake_event.wait(0.1)
2431
2432 def countJobResults(self, jobs, result):
2433 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002434 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002435
Monty Taylor0d926122017-05-24 08:07:56 -05002436 def getBuildByName(self, name):
2437 for build in self.builds:
2438 if build.name == name:
2439 return build
2440 raise Exception("Unable to find build %s" % name)
2441
James E. Blair96c6bf82016-01-15 16:20:40 -08002442 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002443 for job in self.history:
2444 if (job.name == name and
2445 (project is None or
2446 job.parameters['ZUUL_PROJECT'] == project)):
2447 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002448 raise Exception("Unable to find job %s in history" % name)
2449
2450 def assertEmptyQueues(self):
2451 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002452 for tenant in self.sched.abide.tenants.values():
2453 for pipeline in tenant.layout.pipelines.values():
2454 for queue in pipeline.queues:
2455 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002456 print('pipeline %s queue %s contents %s' % (
2457 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002458 self.assertEqual(len(queue.queue), 0,
2459 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002460
2461 def assertReportedStat(self, key, value=None, kind=None):
2462 start = time.time()
2463 while time.time() < (start + 5):
2464 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002465 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002466 if key == k:
2467 if value is None and kind is None:
2468 return
2469 elif value:
2470 if value == v:
2471 return
2472 elif kind:
2473 if v.endswith('|' + kind):
2474 return
2475 time.sleep(0.1)
2476
Clark Boylanb640e052014-04-03 16:41:46 -07002477 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002478
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002479 def assertBuilds(self, builds):
2480 """Assert that the running builds are as described.
2481
2482 The list of running builds is examined and must match exactly
2483 the list of builds described by the input.
2484
2485 :arg list builds: A list of dictionaries. Each item in the
2486 list must match the corresponding build in the build
2487 history, and each element of the dictionary must match the
2488 corresponding attribute of the build.
2489
2490 """
James E. Blair3158e282016-08-19 09:34:11 -07002491 try:
2492 self.assertEqual(len(self.builds), len(builds))
2493 for i, d in enumerate(builds):
2494 for k, v in d.items():
2495 self.assertEqual(
2496 getattr(self.builds[i], k), v,
2497 "Element %i in builds does not match" % (i,))
2498 except Exception:
2499 for build in self.builds:
2500 self.log.error("Running build: %s" % build)
2501 else:
2502 self.log.error("No running builds")
2503 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002504
James E. Blairb536ecc2016-08-31 10:11:42 -07002505 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002506 """Assert that the completed builds are as described.
2507
2508 The list of completed builds is examined and must match
2509 exactly the list of builds described by the input.
2510
2511 :arg list history: A list of dictionaries. Each item in the
2512 list must match the corresponding build in the build
2513 history, and each element of the dictionary must match the
2514 corresponding attribute of the build.
2515
James E. Blairb536ecc2016-08-31 10:11:42 -07002516 :arg bool ordered: If true, the history must match the order
2517 supplied, if false, the builds are permitted to have
2518 arrived in any order.
2519
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002520 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002521 def matches(history_item, item):
2522 for k, v in item.items():
2523 if getattr(history_item, k) != v:
2524 return False
2525 return True
James E. Blair3158e282016-08-19 09:34:11 -07002526 try:
2527 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002528 if ordered:
2529 for i, d in enumerate(history):
2530 if not matches(self.history[i], d):
2531 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002532 "Element %i in history does not match %s" %
2533 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002534 else:
2535 unseen = self.history[:]
2536 for i, d in enumerate(history):
2537 found = False
2538 for unseen_item in unseen:
2539 if matches(unseen_item, d):
2540 found = True
2541 unseen.remove(unseen_item)
2542 break
2543 if not found:
2544 raise Exception("No match found for element %i "
2545 "in history" % (i,))
2546 if unseen:
2547 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002548 except Exception:
2549 for build in self.history:
2550 self.log.error("Completed build: %s" % build)
2551 else:
2552 self.log.error("No completed builds")
2553 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002554
James E. Blair6ac368c2016-12-22 18:07:20 -08002555 def printHistory(self):
2556 """Log the build history.
2557
2558 This can be useful during tests to summarize what jobs have
2559 completed.
2560
2561 """
2562 self.log.debug("Build history:")
2563 for build in self.history:
2564 self.log.debug(build)
2565
James E. Blair59fdbac2015-12-07 17:08:06 -08002566 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002567 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2568
James E. Blair9ea70072017-04-19 16:05:30 -07002569 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002570 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002571 if not os.path.exists(root):
2572 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002573 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2574 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002575- tenant:
2576 name: openstack
2577 source:
2578 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002579 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002580 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002581 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002582 - org/project
2583 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002584 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002585 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002586 self.config.set('zuul', 'tenant_config',
2587 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002588 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002589
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002590 def addCommitToRepo(self, project, message, files,
2591 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002592 path = os.path.join(self.upstream_root, project)
2593 repo = git.Repo(path)
2594 repo.head.reference = branch
2595 zuul.merger.merger.reset_repo_to_head(repo)
2596 for fn, content in files.items():
2597 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002598 try:
2599 os.makedirs(os.path.dirname(fn))
2600 except OSError:
2601 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002602 with open(fn, 'w') as f:
2603 f.write(content)
2604 repo.index.add([fn])
2605 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002606 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002607 repo.heads[branch].commit = commit
2608 repo.head.reference = branch
2609 repo.git.clean('-x', '-f', '-d')
2610 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002611 if tag:
2612 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002613 return before
2614
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002615 def commitConfigUpdate(self, project_name, source_name):
2616 """Commit an update to zuul.yaml
2617
2618 This overwrites the zuul.yaml in the specificed project with
2619 the contents specified.
2620
2621 :arg str project_name: The name of the project containing
2622 zuul.yaml (e.g., common-config)
2623
2624 :arg str source_name: The path to the file (underneath the
2625 test fixture directory) whose contents should be used to
2626 replace zuul.yaml.
2627 """
2628
2629 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002630 files = {}
2631 with open(source_path, 'r') as f:
2632 data = f.read()
2633 layout = yaml.safe_load(data)
2634 files['zuul.yaml'] = data
2635 for item in layout:
2636 if 'job' in item:
2637 jobname = item['job']['name']
2638 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002639 before = self.addCommitToRepo(
2640 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002641 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002642 return before
2643
James E. Blair7fc8daa2016-08-08 15:37:15 -07002644 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002645
James E. Blair7fc8daa2016-08-08 15:37:15 -07002646 """Inject a Fake (Gerrit) event.
2647
2648 This method accepts a JSON-encoded event and simulates Zuul
2649 having received it from Gerrit. It could (and should)
2650 eventually apply to any connection type, but is currently only
2651 used with Gerrit connections. The name of the connection is
2652 used to look up the corresponding server, and the event is
2653 simulated as having been received by all Zuul connections
2654 attached to that server. So if two Gerrit connections in Zuul
2655 are connected to the same Gerrit server, and you invoke this
2656 method specifying the name of one of them, the event will be
2657 received by both.
2658
2659 .. note::
2660
2661 "self.fake_gerrit.addEvent" calls should be migrated to
2662 this method.
2663
2664 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002665 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002666 :arg str event: The JSON-encoded event.
2667
2668 """
2669 specified_conn = self.connections.connections[connection]
2670 for conn in self.connections.connections.values():
2671 if (isinstance(conn, specified_conn.__class__) and
2672 specified_conn.server == conn.server):
2673 conn.addEvent(event)
2674
James E. Blaird8af5422017-05-24 13:59:40 -07002675 def getUpstreamRepos(self, projects):
2676 """Return upstream git repo objects for the listed projects
2677
2678 :arg list projects: A list of strings, each the canonical name
2679 of a project.
2680
2681 :returns: A dictionary of {name: repo} for every listed
2682 project.
2683 :rtype: dict
2684
2685 """
2686
2687 repos = {}
2688 for project in projects:
2689 # FIXME(jeblair): the upstream root does not yet have a
2690 # hostname component; that needs to be added, and this
2691 # line removed:
2692 tmp_project_name = '/'.join(project.split('/')[1:])
2693 path = os.path.join(self.upstream_root, tmp_project_name)
2694 repo = git.Repo(path)
2695 repos[project] = repo
2696 return repos
2697
James E. Blair3f876d52016-07-22 13:07:14 -07002698
2699class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002700 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002701 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002702
Joshua Heskethd78b4482015-09-14 16:56:34 -06002703
2704class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002705 def setup_config(self):
2706 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002707 for section_name in self.config.sections():
2708 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2709 section_name, re.I)
2710 if not con_match:
2711 continue
2712
2713 if self.config.get(section_name, 'driver') == 'sql':
2714 f = MySQLSchemaFixture()
2715 self.useFixture(f)
2716 if (self.config.get(section_name, 'dburi') ==
2717 '$MYSQL_FIXTURE_DBURI$'):
2718 self.config.set(section_name, 'dburi', f.dburi)