blob: ec6ae640806c94b6058657766cb0cdbae103280b [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700557 writers=[], body=''):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700558 """Creates a new PR with several commits.
559 Sends an event about opened PR."""
560 self.github = github
561 self.source = github
562 self.number = number
563 self.project = project
564 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100565 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700566 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100567 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700568 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100569 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700570 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100571 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100572 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800573 self.reviews = []
574 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700575 self.updated_at = None
576 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100577 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100578 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700579 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._updateTimeStamp()
583
Jan Hruban570d01c2016-03-10 21:51:32 +0100584 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700585 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100586 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700587 self._updateTimeStamp()
588
Jan Hruban570d01c2016-03-10 21:51:32 +0100589 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700590 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100591 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700592 self._updateTimeStamp()
593
594 def getPullRequestOpenedEvent(self):
595 return self._getPullRequestEvent('opened')
596
597 def getPullRequestSynchronizeEvent(self):
598 return self._getPullRequestEvent('synchronize')
599
600 def getPullRequestReopenedEvent(self):
601 return self._getPullRequestEvent('reopened')
602
603 def getPullRequestClosedEvent(self):
604 return self._getPullRequestEvent('closed')
605
Jesse Keatinga41566f2017-06-14 18:17:51 -0700606 def getPullRequestEditedEvent(self):
607 return self._getPullRequestEvent('edited')
608
Gregory Haynes4fc12542015-04-22 20:38:06 -0700609 def addComment(self, message):
610 self.comments.append(message)
611 self._updateTimeStamp()
612
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200613 def getCommentAddedEvent(self, text):
614 name = 'issue_comment'
615 data = {
616 'action': 'created',
617 'issue': {
618 'number': self.number
619 },
620 'comment': {
621 'body': text
622 },
623 'repository': {
624 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100625 },
626 'sender': {
627 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200628 }
629 }
630 return (name, data)
631
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800632 def getReviewAddedEvent(self, review):
633 name = 'pull_request_review'
634 data = {
635 'action': 'submitted',
636 'pull_request': {
637 'number': self.number,
638 'title': self.subject,
639 'updated_at': self.updated_at,
640 'base': {
641 'ref': self.branch,
642 'repo': {
643 'full_name': self.project
644 }
645 },
646 'head': {
647 'sha': self.head_sha
648 }
649 },
650 'review': {
651 'state': review
652 },
653 'repository': {
654 'full_name': self.project
655 },
656 'sender': {
657 'login': 'ghuser'
658 }
659 }
660 return (name, data)
661
Jan Hruban16ad31f2015-11-07 14:39:07 +0100662 def addLabel(self, name):
663 if name not in self.labels:
664 self.labels.append(name)
665 self._updateTimeStamp()
666 return self._getLabelEvent(name)
667
668 def removeLabel(self, name):
669 if name in self.labels:
670 self.labels.remove(name)
671 self._updateTimeStamp()
672 return self._getUnlabelEvent(name)
673
674 def _getLabelEvent(self, label):
675 name = 'pull_request'
676 data = {
677 'action': 'labeled',
678 'pull_request': {
679 'number': self.number,
680 'updated_at': self.updated_at,
681 'base': {
682 'ref': self.branch,
683 'repo': {
684 'full_name': self.project
685 }
686 },
687 'head': {
688 'sha': self.head_sha
689 }
690 },
691 'label': {
692 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100693 },
694 'sender': {
695 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100696 }
697 }
698 return (name, data)
699
700 def _getUnlabelEvent(self, label):
701 name = 'pull_request'
702 data = {
703 'action': 'unlabeled',
704 'pull_request': {
705 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100706 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100707 'updated_at': self.updated_at,
708 'base': {
709 'ref': self.branch,
710 'repo': {
711 'full_name': self.project
712 }
713 },
714 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800715 'sha': self.head_sha,
716 'repo': {
717 'full_name': self.project
718 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100719 }
720 },
721 'label': {
722 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100723 },
724 'sender': {
725 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100726 }
727 }
728 return (name, data)
729
Jesse Keatinga41566f2017-06-14 18:17:51 -0700730 def editBody(self, body):
731 self.body = body
732 self._updateTimeStamp()
733
Gregory Haynes4fc12542015-04-22 20:38:06 -0700734 def _getRepo(self):
735 repo_path = os.path.join(self.upstream_root, self.project)
736 return git.Repo(repo_path)
737
738 def _createPRRef(self):
739 repo = self._getRepo()
740 GithubChangeReference.create(
741 repo, self._getPRReference(), 'refs/tags/init')
742
Jan Hruban570d01c2016-03-10 21:51:32 +0100743 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700744 repo = self._getRepo()
745 ref = repo.references[self._getPRReference()]
746 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100747 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700748 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100749 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700750 repo.head.reference = ref
751 zuul.merger.merger.reset_repo_to_head(repo)
752 repo.git.clean('-x', '-f', '-d')
753
Jan Hruban570d01c2016-03-10 21:51:32 +0100754 if files:
755 fn = files[0]
756 self.files = files
757 else:
758 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
759 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100760 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700761 fn = os.path.join(repo.working_dir, fn)
762 f = open(fn, 'w')
763 with open(fn, 'w') as f:
764 f.write("test %s %s\n" %
765 (self.branch, self.number))
766 repo.index.add([fn])
767
768 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800769 # Create an empty set of statuses for the given sha,
770 # each sha on a PR may have a status set on it
771 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 repo.head.reference = 'master'
773 zuul.merger.merger.reset_repo_to_head(repo)
774 repo.git.clean('-x', '-f', '-d')
775 repo.heads['master'].checkout()
776
777 def _updateTimeStamp(self):
778 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
779
780 def getPRHeadSha(self):
781 repo = self._getRepo()
782 return repo.references[self._getPRReference()].commit.hexsha
783
Jesse Keatingae4cd272017-01-30 17:10:44 -0800784 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800785 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
786 # convert the timestamp to a str format that would be returned
787 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800788
Adam Gandelmand81dd762017-02-09 15:15:49 -0800789 if granted_on:
790 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
791 submitted_at = time.strftime(
792 gh_time_format, granted_on.timetuple())
793 else:
794 # github timestamps only down to the second, so we need to make
795 # sure reviews that tests add appear to be added over a period of
796 # time in the past and not all at once.
797 if not self.reviews:
798 # the first review happens 10 mins ago
799 offset = 600
800 else:
801 # subsequent reviews happen 1 minute closer to now
802 offset = 600 - (len(self.reviews) * 60)
803
804 granted_on = datetime.datetime.utcfromtimestamp(
805 time.time() - offset)
806 submitted_at = time.strftime(
807 gh_time_format, granted_on.timetuple())
808
Jesse Keatingae4cd272017-01-30 17:10:44 -0800809 self.reviews.append({
810 'state': state,
811 'user': {
812 'login': user,
813 'email': user + "@derp.com",
814 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800815 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800816 })
817
Gregory Haynes4fc12542015-04-22 20:38:06 -0700818 def _getPRReference(self):
819 return '%s/head' % self.number
820
821 def _getPullRequestEvent(self, action):
822 name = 'pull_request'
823 data = {
824 'action': action,
825 'number': self.number,
826 'pull_request': {
827 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100828 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700829 'updated_at': self.updated_at,
830 'base': {
831 'ref': self.branch,
832 'repo': {
833 'full_name': self.project
834 }
835 },
836 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800837 'sha': self.head_sha,
838 'repo': {
839 'full_name': self.project
840 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700841 },
842 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100843 },
844 'sender': {
845 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700846 }
847 }
848 return (name, data)
849
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800850 def getCommitStatusEvent(self, context, state='success', user='zuul'):
851 name = 'status'
852 data = {
853 'state': state,
854 'sha': self.head_sha,
855 'description': 'Test results for %s: %s' % (self.head_sha, state),
856 'target_url': 'http://zuul/%s' % self.head_sha,
857 'branches': [],
858 'context': context,
859 'sender': {
860 'login': user
861 }
862 }
863 return (name, data)
864
Gregory Haynes4fc12542015-04-22 20:38:06 -0700865
866class FakeGithubConnection(githubconnection.GithubConnection):
867 log = logging.getLogger("zuul.test.FakeGithubConnection")
868
869 def __init__(self, driver, connection_name, connection_config,
870 upstream_root=None):
871 super(FakeGithubConnection, self).__init__(driver, connection_name,
872 connection_config)
873 self.connection_name = connection_name
874 self.pr_number = 0
875 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700876 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100878 self.merge_failure = False
879 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880
Jesse Keatinga41566f2017-06-14 18:17:51 -0700881 def openFakePullRequest(self, project, branch, subject, files=[],
882 body=''):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700883 self.pr_number += 1
884 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100885 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700886 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700887 self.pull_requests.append(pull_request)
888 return pull_request
889
Jesse Keating71a47ff2017-06-06 11:36:43 -0700890 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
891 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700892 if not old_rev:
893 old_rev = '00000000000000000000000000000000'
894 if not new_rev:
895 new_rev = random_sha1()
896 name = 'push'
897 data = {
898 'ref': ref,
899 'before': old_rev,
900 'after': new_rev,
901 'repository': {
902 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700903 },
904 'commits': [
905 {
906 'added': added_files,
907 'removed': removed_files,
908 'modified': modified_files
909 }
910 ]
Wayne1a78c612015-06-11 17:14:13 -0700911 }
912 return (name, data)
913
Gregory Haynes4fc12542015-04-22 20:38:06 -0700914 def emitEvent(self, event):
915 """Emulates sending the GitHub webhook event to the connection."""
916 port = self.webapp.server.socket.getsockname()[1]
917 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700918 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700919 headers = {'X-Github-Event': name}
920 req = urllib.request.Request(
921 'http://localhost:%s/connection/%s/payload'
922 % (port, self.connection_name),
923 data=payload, headers=headers)
924 urllib.request.urlopen(req)
925
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200926 def getPull(self, project, number):
927 pr = self.pull_requests[number - 1]
928 data = {
929 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100930 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200931 'updated_at': pr.updated_at,
932 'base': {
933 'repo': {
934 'full_name': pr.project
935 },
936 'ref': pr.branch,
937 },
Jan Hruban37615e52015-11-19 14:30:49 +0100938 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700939 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200940 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800941 'sha': pr.head_sha,
942 'repo': {
943 'full_name': pr.project
944 }
Jesse Keating61040e72017-06-08 15:08:27 -0700945 },
Jesse Keating19dfb492017-06-13 12:32:33 -0700946 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700947 'labels': pr.labels,
948 'merged': pr.is_merged,
949 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200950 }
951 return data
952
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800953 def getPullBySha(self, sha):
954 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
955 if len(prs) > 1:
956 raise Exception('Multiple pulls found with head sha: %s' % sha)
957 pr = prs[0]
958 return self.getPull(pr.project, pr.number)
959
Jesse Keatingae4cd272017-01-30 17:10:44 -0800960 def _getPullReviews(self, owner, project, number):
961 pr = self.pull_requests[number - 1]
962 return pr.reviews
963
Jan Hruban3b415922016-02-03 13:10:22 +0100964 def getUser(self, login):
965 data = {
966 'username': login,
967 'name': 'Github User',
968 'email': 'github.user@example.com'
969 }
970 return data
971
Jesse Keatingae4cd272017-01-30 17:10:44 -0800972 def getRepoPermission(self, project, login):
973 owner, proj = project.split('/')
974 for pr in self.pull_requests:
975 pr_owner, pr_project = pr.project.split('/')
976 if (pr_owner == owner and proj == pr_project):
977 if login in pr.writers:
978 return 'write'
979 else:
980 return 'read'
981
Gregory Haynes4fc12542015-04-22 20:38:06 -0700982 def getGitUrl(self, project):
983 return os.path.join(self.upstream_root, str(project))
984
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200985 def real_getGitUrl(self, project):
986 return super(FakeGithubConnection, self).getGitUrl(project)
987
Gregory Haynes4fc12542015-04-22 20:38:06 -0700988 def getProjectBranches(self, project):
989 """Masks getProjectBranches since we don't have a real github"""
990
991 # just returns master for now
992 return ['master']
993
Jan Hrubane252a732017-01-03 15:03:09 +0100994 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700995 pull_request = self.pull_requests[pr_number - 1]
996 pull_request.addComment(message)
997
Jan Hruban3b415922016-02-03 13:10:22 +0100998 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100999 pull_request = self.pull_requests[pr_number - 1]
1000 if self.merge_failure:
1001 raise Exception('Pull request was not merged')
1002 if self.merge_not_allowed_count > 0:
1003 self.merge_not_allowed_count -= 1
1004 raise MergeFailure('Merge was not successful due to mergeability'
1005 ' conflict')
1006 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +01001007 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +01001008
Jesse Keatingd96e5882017-01-19 13:55:50 -08001009 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001010 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -08001011
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001012 def setCommitStatus(self, project, sha, state, url='', description='',
1013 context='default', user='zuul'):
1014 # always insert a status to the front of the list, to represent
1015 # the last status provided for a commit.
1016 # Since we're bypassing github API, which would require a user, we
1017 # default the user as 'zuul' here.
1018 self.statuses.setdefault(project, {}).setdefault(sha, [])
1019 self.statuses[project][sha].insert(0, {
1020 'state': state,
1021 'url': url,
1022 'description': description,
1023 'context': context,
1024 'creator': {
1025 'login': user
1026 }
1027 })
Jan Hrubane252a732017-01-03 15:03:09 +01001028
Jan Hruban16ad31f2015-11-07 14:39:07 +01001029 def labelPull(self, project, pr_number, label):
1030 pull_request = self.pull_requests[pr_number - 1]
1031 pull_request.addLabel(label)
1032
1033 def unlabelPull(self, project, pr_number, label):
1034 pull_request = self.pull_requests[pr_number - 1]
1035 pull_request.removeLabel(label)
1036
Jesse Keatinga41566f2017-06-14 18:17:51 -07001037 def _getNeededByFromPR(self, change):
1038 prs = []
1039 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
1040 (self.git_host, change.project.name,
1041 change.number))
1042 for pr in self.pull_requests:
1043 if pattern.search(pr.body):
1044 # Get our version of a pull so that it's a dict
1045 pull = self.getPull(pr.project, pr.number)
1046 prs.append(pull)
1047
1048 return prs
1049
Gregory Haynes4fc12542015-04-22 20:38:06 -07001050
Clark Boylanb640e052014-04-03 16:41:46 -07001051class BuildHistory(object):
1052 def __init__(self, **kw):
1053 self.__dict__.update(kw)
1054
1055 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001056 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1057 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001058
1059
Clark Boylanb640e052014-04-03 16:41:46 -07001060class FakeStatsd(threading.Thread):
1061 def __init__(self):
1062 threading.Thread.__init__(self)
1063 self.daemon = True
1064 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1065 self.sock.bind(('', 0))
1066 self.port = self.sock.getsockname()[1]
1067 self.wake_read, self.wake_write = os.pipe()
1068 self.stats = []
1069
1070 def run(self):
1071 while True:
1072 poll = select.poll()
1073 poll.register(self.sock, select.POLLIN)
1074 poll.register(self.wake_read, select.POLLIN)
1075 ret = poll.poll()
1076 for (fd, event) in ret:
1077 if fd == self.sock.fileno():
1078 data = self.sock.recvfrom(1024)
1079 if not data:
1080 return
1081 self.stats.append(data[0])
1082 if fd == self.wake_read:
1083 return
1084
1085 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001086 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001087
1088
James E. Blaire1767bc2016-08-02 10:00:27 -07001089class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001090 log = logging.getLogger("zuul.test")
1091
Paul Belanger174a8272017-03-14 13:20:10 -04001092 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001093 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001094 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001095 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001096 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001097 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001098 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001099 # TODOv3(jeblair): self.node is really "the label of the node
1100 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001101 # keep using it like this, or we may end up exposing more of
1102 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001103 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001104 self.node = None
1105 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001106 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001107 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001108 self.pipeline = self.parameters['ZUUL_PIPELINE']
1109 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001110 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001111 self.wait_condition = threading.Condition()
1112 self.waiting = False
1113 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001114 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001115 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001116 self.changes = None
1117 if 'ZUUL_CHANGE_IDS' in self.parameters:
1118 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001119
James E. Blair3158e282016-08-19 09:34:11 -07001120 def __repr__(self):
1121 waiting = ''
1122 if self.waiting:
1123 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001124 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1125 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001126
Clark Boylanb640e052014-04-03 16:41:46 -07001127 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001128 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001129 self.wait_condition.acquire()
1130 self.wait_condition.notify()
1131 self.waiting = False
1132 self.log.debug("Build %s released" % self.unique)
1133 self.wait_condition.release()
1134
1135 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001136 """Return whether this build is being held.
1137
1138 :returns: Whether the build is being held.
1139 :rtype: bool
1140 """
1141
Clark Boylanb640e052014-04-03 16:41:46 -07001142 self.wait_condition.acquire()
1143 if self.waiting:
1144 ret = True
1145 else:
1146 ret = False
1147 self.wait_condition.release()
1148 return ret
1149
1150 def _wait(self):
1151 self.wait_condition.acquire()
1152 self.waiting = True
1153 self.log.debug("Build %s waiting" % self.unique)
1154 self.wait_condition.wait()
1155 self.wait_condition.release()
1156
1157 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001158 self.log.debug('Running build %s' % self.unique)
1159
Paul Belanger174a8272017-03-14 13:20:10 -04001160 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.log.debug('Holding build %s' % self.unique)
1162 self._wait()
1163 self.log.debug("Build %s continuing" % self.unique)
1164
James E. Blair412fba82017-01-26 15:00:50 -08001165 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001166 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001167 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001168 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001169 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001170 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001171 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001172
James E. Blaire1767bc2016-08-02 10:00:27 -07001173 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001174
James E. Blaira5dba232016-08-08 15:53:24 -07001175 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001176 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001177 for change in changes:
1178 if self.hasChanges(change):
1179 return True
1180 return False
1181
James E. Blaire7b99a02016-08-05 14:27:34 -07001182 def hasChanges(self, *changes):
1183 """Return whether this build has certain changes in its git repos.
1184
1185 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001186 are expected to be present (in order) in the git repository of
1187 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001188
1189 :returns: Whether the build has the indicated changes.
1190 :rtype: bool
1191
1192 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001193 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001194 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001195 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001196 try:
1197 repo = git.Repo(path)
1198 except NoSuchPathError as e:
1199 self.log.debug('%s' % e)
1200 return False
1201 ref = self.parameters['ZUUL_REF']
1202 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1203 commit_message = '%s-1' % change.subject
1204 self.log.debug("Checking if build %s has changes; commit_message "
1205 "%s; repo_messages %s" % (self, commit_message,
1206 repo_messages))
1207 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001208 self.log.debug(" messages do not match")
1209 return False
1210 self.log.debug(" OK")
1211 return True
1212
James E. Blaird8af5422017-05-24 13:59:40 -07001213 def getWorkspaceRepos(self, projects):
1214 """Return workspace git repo objects for the listed projects
1215
1216 :arg list projects: A list of strings, each the canonical name
1217 of a project.
1218
1219 :returns: A dictionary of {name: repo} for every listed
1220 project.
1221 :rtype: dict
1222
1223 """
1224
1225 repos = {}
1226 for project in projects:
1227 path = os.path.join(self.jobdir.src_root, project)
1228 repo = git.Repo(path)
1229 repos[project] = repo
1230 return repos
1231
Clark Boylanb640e052014-04-03 16:41:46 -07001232
Paul Belanger174a8272017-03-14 13:20:10 -04001233class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1234 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001235
Paul Belanger174a8272017-03-14 13:20:10 -04001236 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001237 they will report that they have started but then pause until
1238 released before reporting completion. This attribute may be
1239 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001240 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001241 be explicitly released.
1242
1243 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001244 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001245 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001246 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001247 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001248 self.hold_jobs_in_build = False
1249 self.lock = threading.Lock()
1250 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001251 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001252 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001253 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001254 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001255
James E. Blaira5dba232016-08-08 15:53:24 -07001256 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001257 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001258
1259 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001260 :arg Change change: The :py:class:`~tests.base.FakeChange`
1261 instance which should cause the job to fail. This job
1262 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001263
1264 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001265 l = self.fail_tests.get(name, [])
1266 l.append(change)
1267 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001268
James E. Blair962220f2016-08-03 11:22:38 -07001269 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001270 """Release a held build.
1271
1272 :arg str regex: A regular expression which, if supplied, will
1273 cause only builds with matching names to be released. If
1274 not supplied, all builds will be released.
1275
1276 """
James E. Blair962220f2016-08-03 11:22:38 -07001277 builds = self.running_builds[:]
1278 self.log.debug("Releasing build %s (%s)" % (regex,
1279 len(self.running_builds)))
1280 for build in builds:
1281 if not regex or re.match(regex, build.name):
1282 self.log.debug("Releasing build %s" %
1283 (build.parameters['ZUUL_UUID']))
1284 build.release()
1285 else:
1286 self.log.debug("Not releasing build %s" %
1287 (build.parameters['ZUUL_UUID']))
1288 self.log.debug("Done releasing builds %s (%s)" %
1289 (regex, len(self.running_builds)))
1290
Paul Belanger174a8272017-03-14 13:20:10 -04001291 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001292 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001293 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001294 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001295 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001296 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001297 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001298 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001299 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1300 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001301
1302 def stopJob(self, job):
1303 self.log.debug("handle stop")
1304 parameters = json.loads(job.arguments)
1305 uuid = parameters['uuid']
1306 for build in self.running_builds:
1307 if build.unique == uuid:
1308 build.aborted = True
1309 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001310 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001311
James E. Blaira002b032017-04-18 10:35:48 -07001312 def stop(self):
1313 for build in self.running_builds:
1314 build.release()
1315 super(RecordingExecutorServer, self).stop()
1316
Joshua Hesketh50c21782016-10-13 21:34:14 +11001317
Paul Belanger174a8272017-03-14 13:20:10 -04001318class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001319 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001320 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001321 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001322 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001323 if not commit: # merge conflict
1324 self.recordResult('MERGER_FAILURE')
1325 return commit
1326
1327 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001328 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001329 self.executor_server.lock.acquire()
1330 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001331 BuildHistory(name=build.name, result=result, changes=build.changes,
1332 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001333 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001334 pipeline=build.parameters['ZUUL_PIPELINE'])
1335 )
Paul Belanger174a8272017-03-14 13:20:10 -04001336 self.executor_server.running_builds.remove(build)
1337 del self.executor_server.job_builds[self.job.unique]
1338 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001339
1340 def runPlaybooks(self, args):
1341 build = self.executor_server.job_builds[self.job.unique]
1342 build.jobdir = self.jobdir
1343
1344 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1345 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001346 return result
1347
Monty Taylore6562aa2017-02-20 07:37:39 -05001348 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001349 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001350
Paul Belanger174a8272017-03-14 13:20:10 -04001351 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001352 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001353 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001354 else:
1355 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001357
James E. Blairad8dca02017-02-21 11:48:32 -05001358 def getHostList(self, args):
1359 self.log.debug("hostlist")
1360 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001361 for host in hosts:
1362 host['host_vars']['ansible_connection'] = 'local'
1363
1364 hosts.append(dict(
1365 name='localhost',
1366 host_vars=dict(ansible_connection='local'),
1367 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001368 return hosts
1369
James E. Blairf5dbd002015-12-23 15:26:17 -08001370
Clark Boylanb640e052014-04-03 16:41:46 -07001371class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001372 """A Gearman server for use in tests.
1373
1374 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1375 added to the queue but will not be distributed to workers
1376 until released. This attribute may be changed at any time and
1377 will take effect for subsequently enqueued jobs, but
1378 previously held jobs will still need to be explicitly
1379 released.
1380
1381 """
1382
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001383 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001384 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001385 if use_ssl:
1386 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1387 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1388 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1389 else:
1390 ssl_ca = None
1391 ssl_cert = None
1392 ssl_key = None
1393
1394 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1395 ssl_cert=ssl_cert,
1396 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001397
1398 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001399 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1400 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001401 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001402 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001403 job.waiting = self.hold_jobs_in_queue
1404 else:
1405 job.waiting = False
1406 if job.waiting:
1407 continue
1408 if job.name in connection.functions:
1409 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001410 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001411 connection.related_jobs[job.handle] = job
1412 job.worker_connection = connection
1413 job.running = True
1414 return job
1415 return None
1416
1417 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001418 """Release a held job.
1419
1420 :arg str regex: A regular expression which, if supplied, will
1421 cause only jobs with matching names to be released. If
1422 not supplied, all jobs will be released.
1423 """
Clark Boylanb640e052014-04-03 16:41:46 -07001424 released = False
1425 qlen = (len(self.high_queue) + len(self.normal_queue) +
1426 len(self.low_queue))
1427 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1428 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001429 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001430 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001431 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001432 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001433 self.log.debug("releasing queued job %s" %
1434 job.unique)
1435 job.waiting = False
1436 released = True
1437 else:
1438 self.log.debug("not releasing queued job %s" %
1439 job.unique)
1440 if released:
1441 self.wakeConnections()
1442 qlen = (len(self.high_queue) + len(self.normal_queue) +
1443 len(self.low_queue))
1444 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1445
1446
1447class FakeSMTP(object):
1448 log = logging.getLogger('zuul.FakeSMTP')
1449
1450 def __init__(self, messages, server, port):
1451 self.server = server
1452 self.port = port
1453 self.messages = messages
1454
1455 def sendmail(self, from_email, to_email, msg):
1456 self.log.info("Sending email from %s, to %s, with msg %s" % (
1457 from_email, to_email, msg))
1458
1459 headers = msg.split('\n\n', 1)[0]
1460 body = msg.split('\n\n', 1)[1]
1461
1462 self.messages.append(dict(
1463 from_email=from_email,
1464 to_email=to_email,
1465 msg=msg,
1466 headers=headers,
1467 body=body,
1468 ))
1469
1470 return True
1471
1472 def quit(self):
1473 return True
1474
1475
James E. Blairdce6cea2016-12-20 16:45:32 -08001476class FakeNodepool(object):
1477 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001478 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001479
1480 log = logging.getLogger("zuul.test.FakeNodepool")
1481
1482 def __init__(self, host, port, chroot):
1483 self.client = kazoo.client.KazooClient(
1484 hosts='%s:%s%s' % (host, port, chroot))
1485 self.client.start()
1486 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001487 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001488 self.thread = threading.Thread(target=self.run)
1489 self.thread.daemon = True
1490 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001491 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001492
1493 def stop(self):
1494 self._running = False
1495 self.thread.join()
1496 self.client.stop()
1497 self.client.close()
1498
1499 def run(self):
1500 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001501 try:
1502 self._run()
1503 except Exception:
1504 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001505 time.sleep(0.1)
1506
1507 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001508 if self.paused:
1509 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001510 for req in self.getNodeRequests():
1511 self.fulfillRequest(req)
1512
1513 def getNodeRequests(self):
1514 try:
1515 reqids = self.client.get_children(self.REQUEST_ROOT)
1516 except kazoo.exceptions.NoNodeError:
1517 return []
1518 reqs = []
1519 for oid in sorted(reqids):
1520 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001521 try:
1522 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001523 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001524 data['_oid'] = oid
1525 reqs.append(data)
1526 except kazoo.exceptions.NoNodeError:
1527 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001528 return reqs
1529
James E. Blaire18d4602017-01-05 11:17:28 -08001530 def getNodes(self):
1531 try:
1532 nodeids = self.client.get_children(self.NODE_ROOT)
1533 except kazoo.exceptions.NoNodeError:
1534 return []
1535 nodes = []
1536 for oid in sorted(nodeids):
1537 path = self.NODE_ROOT + '/' + oid
1538 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001539 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001540 data['_oid'] = oid
1541 try:
1542 lockfiles = self.client.get_children(path + '/lock')
1543 except kazoo.exceptions.NoNodeError:
1544 lockfiles = []
1545 if lockfiles:
1546 data['_lock'] = True
1547 else:
1548 data['_lock'] = False
1549 nodes.append(data)
1550 return nodes
1551
James E. Blaira38c28e2017-01-04 10:33:20 -08001552 def makeNode(self, request_id, node_type):
1553 now = time.time()
1554 path = '/nodepool/nodes/'
1555 data = dict(type=node_type,
1556 provider='test-provider',
1557 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001558 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001559 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001560 public_ipv4='127.0.0.1',
1561 private_ipv4=None,
1562 public_ipv6=None,
1563 allocated_to=request_id,
1564 state='ready',
1565 state_time=now,
1566 created_time=now,
1567 updated_time=now,
1568 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001569 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001570 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001571 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001572 path = self.client.create(path, data,
1573 makepath=True,
1574 sequence=True)
1575 nodeid = path.split("/")[-1]
1576 return nodeid
1577
James E. Blair6ab79e02017-01-06 10:10:17 -08001578 def addFailRequest(self, request):
1579 self.fail_requests.add(request['_oid'])
1580
James E. Blairdce6cea2016-12-20 16:45:32 -08001581 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001582 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001583 return
1584 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001585 oid = request['_oid']
1586 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001587
James E. Blair6ab79e02017-01-06 10:10:17 -08001588 if oid in self.fail_requests:
1589 request['state'] = 'failed'
1590 else:
1591 request['state'] = 'fulfilled'
1592 nodes = []
1593 for node in request['node_types']:
1594 nodeid = self.makeNode(oid, node)
1595 nodes.append(nodeid)
1596 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001597
James E. Blaira38c28e2017-01-04 10:33:20 -08001598 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001599 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001600 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001601 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001602 try:
1603 self.client.set(path, data)
1604 except kazoo.exceptions.NoNodeError:
1605 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001606
1607
James E. Blair498059b2016-12-20 13:50:13 -08001608class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001609 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001610 super(ChrootedKazooFixture, self).__init__()
1611
1612 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1613 if ':' in zk_host:
1614 host, port = zk_host.split(':')
1615 else:
1616 host = zk_host
1617 port = None
1618
1619 self.zookeeper_host = host
1620
1621 if not port:
1622 self.zookeeper_port = 2181
1623 else:
1624 self.zookeeper_port = int(port)
1625
Clark Boylan621ec9a2017-04-07 17:41:33 -07001626 self.test_id = test_id
1627
James E. Blair498059b2016-12-20 13:50:13 -08001628 def _setUp(self):
1629 # Make sure the test chroot paths do not conflict
1630 random_bits = ''.join(random.choice(string.ascii_lowercase +
1631 string.ascii_uppercase)
1632 for x in range(8))
1633
Clark Boylan621ec9a2017-04-07 17:41:33 -07001634 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001635 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1636
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001637 self.addCleanup(self._cleanup)
1638
James E. Blair498059b2016-12-20 13:50:13 -08001639 # Ensure the chroot path exists and clean up any pre-existing znodes.
1640 _tmp_client = kazoo.client.KazooClient(
1641 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1642 _tmp_client.start()
1643
1644 if _tmp_client.exists(self.zookeeper_chroot):
1645 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1646
1647 _tmp_client.ensure_path(self.zookeeper_chroot)
1648 _tmp_client.stop()
1649 _tmp_client.close()
1650
James E. Blair498059b2016-12-20 13:50:13 -08001651 def _cleanup(self):
1652 '''Remove the chroot path.'''
1653 # Need a non-chroot'ed client to remove the chroot path
1654 _tmp_client = kazoo.client.KazooClient(
1655 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1656 _tmp_client.start()
1657 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1658 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001659 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001660
1661
Joshua Heskethd78b4482015-09-14 16:56:34 -06001662class MySQLSchemaFixture(fixtures.Fixture):
1663 def setUp(self):
1664 super(MySQLSchemaFixture, self).setUp()
1665
1666 random_bits = ''.join(random.choice(string.ascii_lowercase +
1667 string.ascii_uppercase)
1668 for x in range(8))
1669 self.name = '%s_%s' % (random_bits, os.getpid())
1670 self.passwd = uuid.uuid4().hex
1671 db = pymysql.connect(host="localhost",
1672 user="openstack_citest",
1673 passwd="openstack_citest",
1674 db="openstack_citest")
1675 cur = db.cursor()
1676 cur.execute("create database %s" % self.name)
1677 cur.execute(
1678 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1679 (self.name, self.name, self.passwd))
1680 cur.execute("flush privileges")
1681
1682 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1683 self.passwd,
1684 self.name)
1685 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1686 self.addCleanup(self.cleanup)
1687
1688 def cleanup(self):
1689 db = pymysql.connect(host="localhost",
1690 user="openstack_citest",
1691 passwd="openstack_citest",
1692 db="openstack_citest")
1693 cur = db.cursor()
1694 cur.execute("drop database %s" % self.name)
1695 cur.execute("drop user '%s'@'localhost'" % self.name)
1696 cur.execute("flush privileges")
1697
1698
Maru Newby3fe5f852015-01-13 04:22:14 +00001699class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001700 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001701 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001702
James E. Blair1c236df2017-02-01 14:07:24 -08001703 def attachLogs(self, *args):
1704 def reader():
1705 self._log_stream.seek(0)
1706 while True:
1707 x = self._log_stream.read(4096)
1708 if not x:
1709 break
1710 yield x.encode('utf8')
1711 content = testtools.content.content_from_reader(
1712 reader,
1713 testtools.content_type.UTF8_TEXT,
1714 False)
1715 self.addDetail('logging', content)
1716
Clark Boylanb640e052014-04-03 16:41:46 -07001717 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001718 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001719 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1720 try:
1721 test_timeout = int(test_timeout)
1722 except ValueError:
1723 # If timeout value is invalid do not set a timeout.
1724 test_timeout = 0
1725 if test_timeout > 0:
1726 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1727
1728 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1729 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1730 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1731 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1732 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1733 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1734 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1735 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1736 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1737 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001738 self._log_stream = StringIO()
1739 self.addOnException(self.attachLogs)
1740 else:
1741 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001742
James E. Blair73b41772017-05-22 13:22:55 -07001743 # NOTE(jeblair): this is temporary extra debugging to try to
1744 # track down a possible leak.
1745 orig_git_repo_init = git.Repo.__init__
1746
1747 def git_repo_init(myself, *args, **kw):
1748 orig_git_repo_init(myself, *args, **kw)
1749 self.log.debug("Created git repo 0x%x %s" %
1750 (id(myself), repr(myself)))
1751
1752 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1753 git_repo_init))
1754
James E. Blair1c236df2017-02-01 14:07:24 -08001755 handler = logging.StreamHandler(self._log_stream)
1756 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1757 '%(levelname)-8s %(message)s')
1758 handler.setFormatter(formatter)
1759
1760 logger = logging.getLogger()
1761 logger.setLevel(logging.DEBUG)
1762 logger.addHandler(handler)
1763
Clark Boylan3410d532017-04-25 12:35:29 -07001764 # Make sure we don't carry old handlers around in process state
1765 # which slows down test runs
1766 self.addCleanup(logger.removeHandler, handler)
1767 self.addCleanup(handler.close)
1768 self.addCleanup(handler.flush)
1769
James E. Blair1c236df2017-02-01 14:07:24 -08001770 # NOTE(notmorgan): Extract logging overrides for specific
1771 # libraries from the OS_LOG_DEFAULTS env and create loggers
1772 # for each. This is used to limit the output during test runs
1773 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001774 log_defaults_from_env = os.environ.get(
1775 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001776 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001777
James E. Blairdce6cea2016-12-20 16:45:32 -08001778 if log_defaults_from_env:
1779 for default in log_defaults_from_env.split(','):
1780 try:
1781 name, level_str = default.split('=', 1)
1782 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001783 logger = logging.getLogger(name)
1784 logger.setLevel(level)
1785 logger.addHandler(handler)
1786 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001787 except ValueError:
1788 # NOTE(notmorgan): Invalid format of the log default,
1789 # skip and don't try and apply a logger for the
1790 # specified module
1791 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001792
Maru Newby3fe5f852015-01-13 04:22:14 +00001793
1794class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001795 """A test case with a functioning Zuul.
1796
1797 The following class variables are used during test setup and can
1798 be overidden by subclasses but are effectively read-only once a
1799 test method starts running:
1800
1801 :cvar str config_file: This points to the main zuul config file
1802 within the fixtures directory. Subclasses may override this
1803 to obtain a different behavior.
1804
1805 :cvar str tenant_config_file: This is the tenant config file
1806 (which specifies from what git repos the configuration should
1807 be loaded). It defaults to the value specified in
1808 `config_file` but can be overidden by subclasses to obtain a
1809 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001810 configuration. See also the :py:func:`simple_layout`
1811 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001812
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001813 :cvar bool create_project_keys: Indicates whether Zuul should
1814 auto-generate keys for each project, or whether the test
1815 infrastructure should insert dummy keys to save time during
1816 startup. Defaults to False.
1817
James E. Blaire7b99a02016-08-05 14:27:34 -07001818 The following are instance variables that are useful within test
1819 methods:
1820
1821 :ivar FakeGerritConnection fake_<connection>:
1822 A :py:class:`~tests.base.FakeGerritConnection` will be
1823 instantiated for each connection present in the config file
1824 and stored here. For instance, `fake_gerrit` will hold the
1825 FakeGerritConnection object for a connection named `gerrit`.
1826
1827 :ivar FakeGearmanServer gearman_server: An instance of
1828 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1829 server that all of the Zuul components in this test use to
1830 communicate with each other.
1831
Paul Belanger174a8272017-03-14 13:20:10 -04001832 :ivar RecordingExecutorServer executor_server: An instance of
1833 :py:class:`~tests.base.RecordingExecutorServer` which is the
1834 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001835
1836 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1837 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001838 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001839 list upon completion.
1840
1841 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1842 objects representing completed builds. They are appended to
1843 the list in the order they complete.
1844
1845 """
1846
James E. Blair83005782015-12-11 14:46:03 -08001847 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001848 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001849 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001850 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001851
1852 def _startMerger(self):
1853 self.merge_server = zuul.merger.server.MergeServer(self.config,
1854 self.connections)
1855 self.merge_server.start()
1856
Maru Newby3fe5f852015-01-13 04:22:14 +00001857 def setUp(self):
1858 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001859
1860 self.setupZK()
1861
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001862 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001863 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001864 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1865 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001866 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001867 tmp_root = tempfile.mkdtemp(
1868 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001869 self.test_root = os.path.join(tmp_root, "zuul-test")
1870 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001871 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001872 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001873 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001874
1875 if os.path.exists(self.test_root):
1876 shutil.rmtree(self.test_root)
1877 os.makedirs(self.test_root)
1878 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001879 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001880
1881 # Make per test copy of Configuration.
1882 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001883 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1884 if not os.path.exists(self.private_key_file):
1885 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1886 shutil.copy(src_private_key_file, self.private_key_file)
1887 shutil.copy('{}.pub'.format(src_private_key_file),
1888 '{}.pub'.format(self.private_key_file))
1889 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001890 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001891 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001892 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001893 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001894 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001895 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001896 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001897
Clark Boylanb640e052014-04-03 16:41:46 -07001898 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001899 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1900 # see: https://github.com/jsocol/pystatsd/issues/61
1901 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001902 os.environ['STATSD_PORT'] = str(self.statsd.port)
1903 self.statsd.start()
1904 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001905 importlib.reload(statsd)
1906 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001907
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001908 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001909
1910 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001911 self.log.info("Gearman server on port %s" %
1912 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001913 if self.use_ssl:
1914 self.log.info('SSL enabled for gearman')
1915 self.config.set(
1916 'gearman', 'ssl_ca',
1917 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1918 self.config.set(
1919 'gearman', 'ssl_cert',
1920 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1921 self.config.set(
1922 'gearman', 'ssl_key',
1923 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001924
James E. Blaire511d2f2016-12-08 15:22:26 -08001925 gerritsource.GerritSource.replication_timeout = 1.5
1926 gerritsource.GerritSource.replication_retry_interval = 0.5
1927 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001928
Joshua Hesketh352264b2015-08-11 23:42:08 +10001929 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001930
Jan Hruban7083edd2015-08-21 14:00:54 +02001931 self.webapp = zuul.webapp.WebApp(
1932 self.sched, port=0, listen_address='127.0.0.1')
1933
Jan Hruban6b71aff2015-10-22 16:58:08 +02001934 self.event_queues = [
1935 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001936 self.sched.trigger_event_queue,
1937 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001938 ]
1939
James E. Blairfef78942016-03-11 16:28:56 -08001940 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001941 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001942
Paul Belanger174a8272017-03-14 13:20:10 -04001943 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001944 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001945 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001946 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001947 _test_root=self.test_root,
1948 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001949 self.executor_server.start()
1950 self.history = self.executor_server.build_history
1951 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001952
Paul Belanger174a8272017-03-14 13:20:10 -04001953 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001954 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001955 self.merge_client = zuul.merger.client.MergeClient(
1956 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001957 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001958 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001959 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001960
James E. Blair0d5a36e2017-02-21 10:53:44 -05001961 self.fake_nodepool = FakeNodepool(
1962 self.zk_chroot_fixture.zookeeper_host,
1963 self.zk_chroot_fixture.zookeeper_port,
1964 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001965
Paul Belanger174a8272017-03-14 13:20:10 -04001966 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001967 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001968 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001969 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001970
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001971 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001972
1973 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001974 self.webapp.start()
1975 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001976 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001977 # Cleanups are run in reverse order
1978 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001979 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001980 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001981
James E. Blairb9c0d772017-03-03 14:34:49 -08001982 self.sched.reconfigure(self.config)
1983 self.sched.resume()
1984
Tobias Henkel7df274b2017-05-26 17:41:11 +02001985 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001986 # Set up gerrit related fakes
1987 # Set a changes database so multiple FakeGerrit's can report back to
1988 # a virtual canonical database given by the configured hostname
1989 self.gerrit_changes_dbs = {}
1990
1991 def getGerritConnection(driver, name, config):
1992 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1993 con = FakeGerritConnection(driver, name, config,
1994 changes_db=db,
1995 upstream_root=self.upstream_root)
1996 self.event_queues.append(con.event_queue)
1997 setattr(self, 'fake_' + name, con)
1998 return con
1999
2000 self.useFixture(fixtures.MonkeyPatch(
2001 'zuul.driver.gerrit.GerritDriver.getConnection',
2002 getGerritConnection))
2003
Gregory Haynes4fc12542015-04-22 20:38:06 -07002004 def getGithubConnection(driver, name, config):
2005 con = FakeGithubConnection(driver, name, config,
2006 upstream_root=self.upstream_root)
2007 setattr(self, 'fake_' + name, con)
2008 return con
2009
2010 self.useFixture(fixtures.MonkeyPatch(
2011 'zuul.driver.github.GithubDriver.getConnection',
2012 getGithubConnection))
2013
James E. Blaire511d2f2016-12-08 15:22:26 -08002014 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002015 # TODO(jhesketh): This should come from lib.connections for better
2016 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002017 # Register connections from the config
2018 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002019
Joshua Hesketh352264b2015-08-11 23:42:08 +10002020 def FakeSMTPFactory(*args, **kw):
2021 args = [self.smtp_messages] + list(args)
2022 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002023
Joshua Hesketh352264b2015-08-11 23:42:08 +10002024 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002025
James E. Blaire511d2f2016-12-08 15:22:26 -08002026 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002027 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002028 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002029
James E. Blair83005782015-12-11 14:46:03 -08002030 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002031 # This creates the per-test configuration object. It can be
2032 # overriden by subclasses, but should not need to be since it
2033 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002034 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002035 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002036
2037 if not self.setupSimpleLayout():
2038 if hasattr(self, 'tenant_config_file'):
2039 self.config.set('zuul', 'tenant_config',
2040 self.tenant_config_file)
2041 git_path = os.path.join(
2042 os.path.dirname(
2043 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2044 'git')
2045 if os.path.exists(git_path):
2046 for reponame in os.listdir(git_path):
2047 project = reponame.replace('_', '/')
2048 self.copyDirToRepo(project,
2049 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002050 # Make test_root persist after ansible run for .flag test
2051 self.config.set('executor', 'trusted_rw_dirs', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002052 self.setupAllProjectKeys()
2053
James E. Blair06cc3922017-04-19 10:08:10 -07002054 def setupSimpleLayout(self):
2055 # If the test method has been decorated with a simple_layout,
2056 # use that instead of the class tenant_config_file. Set up a
2057 # single config-project with the specified layout, and
2058 # initialize repos for all of the 'project' entries which
2059 # appear in the layout.
2060 test_name = self.id().split('.')[-1]
2061 test = getattr(self, test_name)
2062 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002063 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002064 else:
2065 return False
2066
James E. Blairb70e55a2017-04-19 12:57:02 -07002067 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002068 path = os.path.join(FIXTURE_DIR, path)
2069 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002070 data = f.read()
2071 layout = yaml.safe_load(data)
2072 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002073 untrusted_projects = []
2074 for item in layout:
2075 if 'project' in item:
2076 name = item['project']['name']
2077 untrusted_projects.append(name)
2078 self.init_repo(name)
2079 self.addCommitToRepo(name, 'initial commit',
2080 files={'README': ''},
2081 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002082 if 'job' in item:
2083 jobname = item['job']['name']
2084 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002085
2086 root = os.path.join(self.test_root, "config")
2087 if not os.path.exists(root):
2088 os.makedirs(root)
2089 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2090 config = [{'tenant':
2091 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002092 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002093 {'config-projects': ['common-config'],
2094 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002095 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002096 f.close()
2097 self.config.set('zuul', 'tenant_config',
2098 os.path.join(FIXTURE_DIR, f.name))
2099
2100 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002101 self.addCommitToRepo('common-config', 'add content from fixture',
2102 files, branch='master', tag='init')
2103
2104 return True
2105
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002106 def setupAllProjectKeys(self):
2107 if self.create_project_keys:
2108 return
2109
2110 path = self.config.get('zuul', 'tenant_config')
2111 with open(os.path.join(FIXTURE_DIR, path)) as f:
2112 tenant_config = yaml.safe_load(f.read())
2113 for tenant in tenant_config:
2114 sources = tenant['tenant']['source']
2115 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002116 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002117 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002118 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002119 self.setupProjectKeys(source, project)
2120
2121 def setupProjectKeys(self, source, project):
2122 # Make sure we set up an RSA key for the project so that we
2123 # don't spend time generating one:
2124
2125 key_root = os.path.join(self.state_root, 'keys')
2126 if not os.path.isdir(key_root):
2127 os.mkdir(key_root, 0o700)
2128 private_key_file = os.path.join(key_root, source, project + '.pem')
2129 private_key_dir = os.path.dirname(private_key_file)
2130 self.log.debug("Installing test keys for project %s at %s" % (
2131 project, private_key_file))
2132 if not os.path.isdir(private_key_dir):
2133 os.makedirs(private_key_dir)
2134 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2135 with open(private_key_file, 'w') as o:
2136 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002137
James E. Blair498059b2016-12-20 13:50:13 -08002138 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002139 self.zk_chroot_fixture = self.useFixture(
2140 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002141 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002142 self.zk_chroot_fixture.zookeeper_host,
2143 self.zk_chroot_fixture.zookeeper_port,
2144 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002145
James E. Blair96c6bf82016-01-15 16:20:40 -08002146 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002147 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002148
2149 files = {}
2150 for (dirpath, dirnames, filenames) in os.walk(source_path):
2151 for filename in filenames:
2152 test_tree_filepath = os.path.join(dirpath, filename)
2153 common_path = os.path.commonprefix([test_tree_filepath,
2154 source_path])
2155 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2156 with open(test_tree_filepath, 'r') as f:
2157 content = f.read()
2158 files[relative_filepath] = content
2159 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002160 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002161
James E. Blaire18d4602017-01-05 11:17:28 -08002162 def assertNodepoolState(self):
2163 # Make sure that there are no pending requests
2164
2165 requests = self.fake_nodepool.getNodeRequests()
2166 self.assertEqual(len(requests), 0)
2167
2168 nodes = self.fake_nodepool.getNodes()
2169 for node in nodes:
2170 self.assertFalse(node['_lock'], "Node %s is locked" %
2171 (node['_oid'],))
2172
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002173 def assertNoGeneratedKeys(self):
2174 # Make sure that Zuul did not generate any project keys
2175 # (unless it was supposed to).
2176
2177 if self.create_project_keys:
2178 return
2179
2180 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2181 test_key = i.read()
2182
2183 key_root = os.path.join(self.state_root, 'keys')
2184 for root, dirname, files in os.walk(key_root):
2185 for fn in files:
2186 with open(os.path.join(root, fn)) as f:
2187 self.assertEqual(test_key, f.read())
2188
Clark Boylanb640e052014-04-03 16:41:46 -07002189 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002190 self.log.debug("Assert final state")
2191 # Make sure no jobs are running
2192 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002193 # Make sure that git.Repo objects have been garbage collected.
2194 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002195 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002196 gc.collect()
2197 for obj in gc.get_objects():
2198 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002199 self.log.debug("Leaked git repo object: 0x%x %s" %
2200 (id(obj), repr(obj)))
2201 for ref in gc.get_referrers(obj):
2202 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002203 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002204 if repos:
2205 for obj in gc.garbage:
2206 self.log.debug(" Garbage %s" % (repr(obj)))
2207 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002208 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002209 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002210 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002211 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002212 for tenant in self.sched.abide.tenants.values():
2213 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002214 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002215 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002216
2217 def shutdown(self):
2218 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002219 self.executor_server.hold_jobs_in_build = False
2220 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002221 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002222 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002223 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002224 self.sched.stop()
2225 self.sched.join()
2226 self.statsd.stop()
2227 self.statsd.join()
2228 self.webapp.stop()
2229 self.webapp.join()
2230 self.rpc.stop()
2231 self.rpc.join()
2232 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002233 self.fake_nodepool.stop()
2234 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002235 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002236 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002237 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002238 # Further the pydevd threads also need to be whitelisted so debugging
2239 # e.g. in PyCharm is possible without breaking shutdown.
2240 whitelist = ['executor-watchdog',
2241 'pydevd.CommandThread',
2242 'pydevd.Reader',
2243 'pydevd.Writer',
2244 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002245 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002246 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002247 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002248 log_str = ""
2249 for thread_id, stack_frame in sys._current_frames().items():
2250 log_str += "Thread: %s\n" % thread_id
2251 log_str += "".join(traceback.format_stack(stack_frame))
2252 self.log.debug(log_str)
2253 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002254
James E. Blaira002b032017-04-18 10:35:48 -07002255 def assertCleanShutdown(self):
2256 pass
2257
James E. Blairc4ba97a2017-04-19 16:26:24 -07002258 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002259 parts = project.split('/')
2260 path = os.path.join(self.upstream_root, *parts[:-1])
2261 if not os.path.exists(path):
2262 os.makedirs(path)
2263 path = os.path.join(self.upstream_root, project)
2264 repo = git.Repo.init(path)
2265
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002266 with repo.config_writer() as config_writer:
2267 config_writer.set_value('user', 'email', 'user@example.com')
2268 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002269
Clark Boylanb640e052014-04-03 16:41:46 -07002270 repo.index.commit('initial commit')
2271 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002272 if tag:
2273 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002274
James E. Blair97d902e2014-08-21 13:25:56 -07002275 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002276 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002277 repo.git.clean('-x', '-f', '-d')
2278
James E. Blair97d902e2014-08-21 13:25:56 -07002279 def create_branch(self, project, branch):
2280 path = os.path.join(self.upstream_root, project)
2281 repo = git.Repo.init(path)
2282 fn = os.path.join(path, 'README')
2283
2284 branch_head = repo.create_head(branch)
2285 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002286 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002287 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002288 f.close()
2289 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002290 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002291
James E. Blair97d902e2014-08-21 13:25:56 -07002292 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002293 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002294 repo.git.clean('-x', '-f', '-d')
2295
Sachi King9f16d522016-03-16 12:20:45 +11002296 def create_commit(self, project):
2297 path = os.path.join(self.upstream_root, project)
2298 repo = git.Repo(path)
2299 repo.head.reference = repo.heads['master']
2300 file_name = os.path.join(path, 'README')
2301 with open(file_name, 'a') as f:
2302 f.write('creating fake commit\n')
2303 repo.index.add([file_name])
2304 commit = repo.index.commit('Creating a fake commit')
2305 return commit.hexsha
2306
James E. Blairf4a5f022017-04-18 14:01:10 -07002307 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002308 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002309 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002310 while len(self.builds):
2311 self.release(self.builds[0])
2312 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002313 i += 1
2314 if count is not None and i >= count:
2315 break
James E. Blairb8c16472015-05-05 14:55:26 -07002316
Clark Boylanb640e052014-04-03 16:41:46 -07002317 def release(self, job):
2318 if isinstance(job, FakeBuild):
2319 job.release()
2320 else:
2321 job.waiting = False
2322 self.log.debug("Queued job %s released" % job.unique)
2323 self.gearman_server.wakeConnections()
2324
2325 def getParameter(self, job, name):
2326 if isinstance(job, FakeBuild):
2327 return job.parameters[name]
2328 else:
2329 parameters = json.loads(job.arguments)
2330 return parameters[name]
2331
Clark Boylanb640e052014-04-03 16:41:46 -07002332 def haveAllBuildsReported(self):
2333 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002334 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002335 return False
2336 # Find out if every build that the worker has completed has been
2337 # reported back to Zuul. If it hasn't then that means a Gearman
2338 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002339 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002340 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002341 if not zbuild:
2342 # It has already been reported
2343 continue
2344 # It hasn't been reported yet.
2345 return False
2346 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002347 worker = self.executor_server.executor_worker
2348 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002349 if connection.state == 'GRAB_WAIT':
2350 return False
2351 return True
2352
2353 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002354 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002355 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002356 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002357 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002358 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002359 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002360 for j in conn.related_jobs.values():
2361 if j.unique == build.uuid:
2362 client_job = j
2363 break
2364 if not client_job:
2365 self.log.debug("%s is not known to the gearman client" %
2366 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002367 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002368 if not client_job.handle:
2369 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002370 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002371 server_job = self.gearman_server.jobs.get(client_job.handle)
2372 if not server_job:
2373 self.log.debug("%s is not known to the gearman server" %
2374 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002375 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002376 if not hasattr(server_job, 'waiting'):
2377 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002378 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002379 if server_job.waiting:
2380 continue
James E. Blair17302972016-08-10 16:11:42 -07002381 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002382 self.log.debug("%s has not reported start" % build)
2383 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002384 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002385 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002386 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002387 if worker_build:
2388 if worker_build.isWaiting():
2389 continue
2390 else:
2391 self.log.debug("%s is running" % worker_build)
2392 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002393 else:
James E. Blair962220f2016-08-03 11:22:38 -07002394 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002395 return False
James E. Blaira002b032017-04-18 10:35:48 -07002396 for (build_uuid, job_worker) in \
2397 self.executor_server.job_workers.items():
2398 if build_uuid not in seen_builds:
2399 self.log.debug("%s is not finalized" % build_uuid)
2400 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002401 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002402
James E. Blairdce6cea2016-12-20 16:45:32 -08002403 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002404 if self.fake_nodepool.paused:
2405 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002406 if self.sched.nodepool.requests:
2407 return False
2408 return True
2409
Jan Hruban6b71aff2015-10-22 16:58:08 +02002410 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002411 for event_queue in self.event_queues:
2412 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002413
2414 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002415 for event_queue in self.event_queues:
2416 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002417
Clark Boylanb640e052014-04-03 16:41:46 -07002418 def waitUntilSettled(self):
2419 self.log.debug("Waiting until settled...")
2420 start = time.time()
2421 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002422 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002423 self.log.error("Timeout waiting for Zuul to settle")
2424 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002425 for event_queue in self.event_queues:
2426 self.log.error(" %s: %s" %
2427 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002428 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002429 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002430 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002431 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002432 self.log.error("All requests completed: %s" %
2433 (self.areAllNodeRequestsComplete(),))
2434 self.log.error("Merge client jobs: %s" %
2435 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002436 raise Exception("Timeout waiting for Zuul to settle")
2437 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002438
Paul Belanger174a8272017-03-14 13:20:10 -04002439 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002440 # have all build states propogated to zuul?
2441 if self.haveAllBuildsReported():
2442 # Join ensures that the queue is empty _and_ events have been
2443 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002444 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002445 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002446 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002447 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002448 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002449 self.areAllNodeRequestsComplete() and
2450 all(self.eventQueuesEmpty())):
2451 # The queue empty check is placed at the end to
2452 # ensure that if a component adds an event between
2453 # when locked the run handler and checked that the
2454 # components were stable, we don't erroneously
2455 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002456 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002457 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002458 self.log.debug("...settled.")
2459 return
2460 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002461 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002462 self.sched.wake_event.wait(0.1)
2463
2464 def countJobResults(self, jobs, result):
2465 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002466 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002467
Monty Taylor0d926122017-05-24 08:07:56 -05002468 def getBuildByName(self, name):
2469 for build in self.builds:
2470 if build.name == name:
2471 return build
2472 raise Exception("Unable to find build %s" % name)
2473
James E. Blair96c6bf82016-01-15 16:20:40 -08002474 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002475 for job in self.history:
2476 if (job.name == name and
2477 (project is None or
2478 job.parameters['ZUUL_PROJECT'] == project)):
2479 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002480 raise Exception("Unable to find job %s in history" % name)
2481
2482 def assertEmptyQueues(self):
2483 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002484 for tenant in self.sched.abide.tenants.values():
2485 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002486 for pipeline_queue in pipeline.queues:
2487 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002488 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002489 pipeline.name, pipeline_queue.name,
2490 pipeline_queue.queue))
2491 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002492 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002493
2494 def assertReportedStat(self, key, value=None, kind=None):
2495 start = time.time()
2496 while time.time() < (start + 5):
2497 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002498 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002499 if key == k:
2500 if value is None and kind is None:
2501 return
2502 elif value:
2503 if value == v:
2504 return
2505 elif kind:
2506 if v.endswith('|' + kind):
2507 return
2508 time.sleep(0.1)
2509
Clark Boylanb640e052014-04-03 16:41:46 -07002510 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002511
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002512 def assertBuilds(self, builds):
2513 """Assert that the running builds are as described.
2514
2515 The list of running builds is examined and must match exactly
2516 the list of builds described by the input.
2517
2518 :arg list builds: A list of dictionaries. Each item in the
2519 list must match the corresponding build in the build
2520 history, and each element of the dictionary must match the
2521 corresponding attribute of the build.
2522
2523 """
James E. Blair3158e282016-08-19 09:34:11 -07002524 try:
2525 self.assertEqual(len(self.builds), len(builds))
2526 for i, d in enumerate(builds):
2527 for k, v in d.items():
2528 self.assertEqual(
2529 getattr(self.builds[i], k), v,
2530 "Element %i in builds does not match" % (i,))
2531 except Exception:
2532 for build in self.builds:
2533 self.log.error("Running build: %s" % build)
2534 else:
2535 self.log.error("No running builds")
2536 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002537
James E. Blairb536ecc2016-08-31 10:11:42 -07002538 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002539 """Assert that the completed builds are as described.
2540
2541 The list of completed builds is examined and must match
2542 exactly the list of builds described by the input.
2543
2544 :arg list history: A list of dictionaries. Each item in the
2545 list must match the corresponding build in the build
2546 history, and each element of the dictionary must match the
2547 corresponding attribute of the build.
2548
James E. Blairb536ecc2016-08-31 10:11:42 -07002549 :arg bool ordered: If true, the history must match the order
2550 supplied, if false, the builds are permitted to have
2551 arrived in any order.
2552
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002553 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002554 def matches(history_item, item):
2555 for k, v in item.items():
2556 if getattr(history_item, k) != v:
2557 return False
2558 return True
James E. Blair3158e282016-08-19 09:34:11 -07002559 try:
2560 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002561 if ordered:
2562 for i, d in enumerate(history):
2563 if not matches(self.history[i], d):
2564 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002565 "Element %i in history does not match %s" %
2566 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002567 else:
2568 unseen = self.history[:]
2569 for i, d in enumerate(history):
2570 found = False
2571 for unseen_item in unseen:
2572 if matches(unseen_item, d):
2573 found = True
2574 unseen.remove(unseen_item)
2575 break
2576 if not found:
2577 raise Exception("No match found for element %i "
2578 "in history" % (i,))
2579 if unseen:
2580 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002581 except Exception:
2582 for build in self.history:
2583 self.log.error("Completed build: %s" % build)
2584 else:
2585 self.log.error("No completed builds")
2586 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002587
James E. Blair6ac368c2016-12-22 18:07:20 -08002588 def printHistory(self):
2589 """Log the build history.
2590
2591 This can be useful during tests to summarize what jobs have
2592 completed.
2593
2594 """
2595 self.log.debug("Build history:")
2596 for build in self.history:
2597 self.log.debug(build)
2598
James E. Blair59fdbac2015-12-07 17:08:06 -08002599 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002600 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2601
James E. Blair9ea70072017-04-19 16:05:30 -07002602 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002603 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002604 if not os.path.exists(root):
2605 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002606 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2607 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002608- tenant:
2609 name: openstack
2610 source:
2611 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002612 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002613 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002614 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002615 - org/project
2616 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002617 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002618 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002619 self.config.set('zuul', 'tenant_config',
2620 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002621 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002622
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002623 def addCommitToRepo(self, project, message, files,
2624 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002625 path = os.path.join(self.upstream_root, project)
2626 repo = git.Repo(path)
2627 repo.head.reference = branch
2628 zuul.merger.merger.reset_repo_to_head(repo)
2629 for fn, content in files.items():
2630 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002631 try:
2632 os.makedirs(os.path.dirname(fn))
2633 except OSError:
2634 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002635 with open(fn, 'w') as f:
2636 f.write(content)
2637 repo.index.add([fn])
2638 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002639 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002640 repo.heads[branch].commit = commit
2641 repo.head.reference = branch
2642 repo.git.clean('-x', '-f', '-d')
2643 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002644 if tag:
2645 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002646 return before
2647
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002648 def commitConfigUpdate(self, project_name, source_name):
2649 """Commit an update to zuul.yaml
2650
2651 This overwrites the zuul.yaml in the specificed project with
2652 the contents specified.
2653
2654 :arg str project_name: The name of the project containing
2655 zuul.yaml (e.g., common-config)
2656
2657 :arg str source_name: The path to the file (underneath the
2658 test fixture directory) whose contents should be used to
2659 replace zuul.yaml.
2660 """
2661
2662 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002663 files = {}
2664 with open(source_path, 'r') as f:
2665 data = f.read()
2666 layout = yaml.safe_load(data)
2667 files['zuul.yaml'] = data
2668 for item in layout:
2669 if 'job' in item:
2670 jobname = item['job']['name']
2671 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002672 before = self.addCommitToRepo(
2673 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002674 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002675 return before
2676
James E. Blair7fc8daa2016-08-08 15:37:15 -07002677 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002678
James E. Blair7fc8daa2016-08-08 15:37:15 -07002679 """Inject a Fake (Gerrit) event.
2680
2681 This method accepts a JSON-encoded event and simulates Zuul
2682 having received it from Gerrit. It could (and should)
2683 eventually apply to any connection type, but is currently only
2684 used with Gerrit connections. The name of the connection is
2685 used to look up the corresponding server, and the event is
2686 simulated as having been received by all Zuul connections
2687 attached to that server. So if two Gerrit connections in Zuul
2688 are connected to the same Gerrit server, and you invoke this
2689 method specifying the name of one of them, the event will be
2690 received by both.
2691
2692 .. note::
2693
2694 "self.fake_gerrit.addEvent" calls should be migrated to
2695 this method.
2696
2697 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002698 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002699 :arg str event: The JSON-encoded event.
2700
2701 """
2702 specified_conn = self.connections.connections[connection]
2703 for conn in self.connections.connections.values():
2704 if (isinstance(conn, specified_conn.__class__) and
2705 specified_conn.server == conn.server):
2706 conn.addEvent(event)
2707
James E. Blaird8af5422017-05-24 13:59:40 -07002708 def getUpstreamRepos(self, projects):
2709 """Return upstream git repo objects for the listed projects
2710
2711 :arg list projects: A list of strings, each the canonical name
2712 of a project.
2713
2714 :returns: A dictionary of {name: repo} for every listed
2715 project.
2716 :rtype: dict
2717
2718 """
2719
2720 repos = {}
2721 for project in projects:
2722 # FIXME(jeblair): the upstream root does not yet have a
2723 # hostname component; that needs to be added, and this
2724 # line removed:
2725 tmp_project_name = '/'.join(project.split('/')[1:])
2726 path = os.path.join(self.upstream_root, tmp_project_name)
2727 repo = git.Repo(path)
2728 repos[project] = repo
2729 return repos
2730
James E. Blair3f876d52016-07-22 13:07:14 -07002731
2732class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002733 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002734 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002735
Joshua Heskethd78b4482015-09-14 16:56:34 -06002736
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002737class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002738 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002739 use_ssl = True
2740
2741
Joshua Heskethd78b4482015-09-14 16:56:34 -06002742class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002743 def setup_config(self):
2744 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002745 for section_name in self.config.sections():
2746 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2747 section_name, re.I)
2748 if not con_match:
2749 continue
2750
2751 if self.config.get(section_name, 'driver') == 'sql':
2752 f = MySQLSchemaFixture()
2753 self.useFixture(f)
2754 if (self.config.get(section_name, 'dburi') ==
2755 '$MYSQL_FIXTURE_DBURI$'):
2756 self.config.set(section_name, 'dburi', f.dburi)