blob: d89469f8742a677e0dff006e337f321b34922f6a [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Adam Gandelmand81dd762017-02-09 15:15:49 -080019import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070020import gc
21import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050022import importlib
23from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import statsd
51import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080052import testtools.content
53import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080054from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000055import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070056
James E. Blaire511d2f2016-12-08 15:22:26 -080057import zuul.driver.gerrit.gerritsource as gerritsource
58import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070059import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070060import zuul.scheduler
61import zuul.webapp
62import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080071import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010072from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700136 categories = {'approved': ('Approved', -1, 1),
137 'code-review': ('Code-Review', -2, 2),
138 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700139
140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700141 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700176 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700180 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700183 ref = GerritChangeReference.create(
184 repo, '1/%s/%s' % (self.number, self.latest_patchset),
185 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700186 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700187 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.git.clean('-x', '-f', '-d')
189
190 path = os.path.join(self.upstream_root, self.project)
191 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700192 for fn, content in files.items():
193 fn = os.path.join(path, fn)
194 with open(fn, 'w') as f:
195 f.write(content)
196 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700197 else:
198 for fni in range(100):
199 fn = os.path.join(path, str(fni))
200 f = open(fn, 'w')
201 for ci in range(4096):
202 f.write(random.choice(string.printable))
203 f.close()
204 repo.index.add([fn])
205
206 r = repo.index.commit(msg)
207 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700208 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700209 repo.git.clean('-x', '-f', '-d')
210 repo.heads['master'].checkout()
211 return r
212
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700213 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700214 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700215 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700216 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700217 data = ("test %s %s %s\n" %
218 (self.branch, self.number, self.latest_patchset))
219 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700220 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700221 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 ps_files = [{'file': '/COMMIT_MSG',
223 'type': 'ADDED'},
224 {'file': 'README',
225 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700227 ps_files.append({'file': f, 'type': 'ADDED'})
228 d = {'approvals': [],
229 'createdOn': time.time(),
230 'files': ps_files,
231 'number': str(self.latest_patchset),
232 'ref': 'refs/changes/1/%s/%s' % (self.number,
233 self.latest_patchset),
234 'revision': c.hexsha,
235 'uploader': {'email': 'user@example.com',
236 'name': 'User name',
237 'username': 'user'}}
238 self.data['currentPatchSet'] = d
239 self.patchsets.append(d)
240 self.data['submitRecords'] = self.getSubmitRecords()
241
242 def getPatchsetCreatedEvent(self, patchset):
243 event = {"type": "patchset-created",
244 "change": {"project": self.project,
245 "branch": self.branch,
246 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
247 "number": str(self.number),
248 "subject": self.subject,
249 "owner": {"name": "User Name"},
250 "url": "https://hostname/3"},
251 "patchSet": self.patchsets[patchset - 1],
252 "uploader": {"name": "User Name"}}
253 return event
254
255 def getChangeRestoredEvent(self):
256 event = {"type": "change-restored",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100265 "patchSet": self.patchsets[-1],
266 "reason": ""}
267 return event
268
269 def getChangeAbandonedEvent(self):
270 event = {"type": "change-abandoned",
271 "change": {"project": self.project,
272 "branch": self.branch,
273 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
274 "number": str(self.number),
275 "subject": self.subject,
276 "owner": {"name": "User Name"},
277 "url": "https://hostname/3"},
278 "abandoner": {"name": "User Name"},
279 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700280 "reason": ""}
281 return event
282
283 def getChangeCommentEvent(self, patchset):
284 event = {"type": "comment-added",
285 "change": {"project": self.project,
286 "branch": self.branch,
287 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
288 "number": str(self.number),
289 "subject": self.subject,
290 "owner": {"name": "User Name"},
291 "url": "https://hostname/3"},
292 "patchSet": self.patchsets[patchset - 1],
293 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700294 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700295 "description": "Code-Review",
296 "value": "0"}],
297 "comment": "This is a comment"}
298 return event
299
James E. Blairc2a5ed72017-02-20 14:12:01 -0500300 def getChangeMergedEvent(self):
301 event = {"submitter": {"name": "Jenkins",
302 "username": "jenkins"},
303 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
304 "patchSet": self.patchsets[-1],
305 "change": self.data,
306 "type": "change-merged",
307 "eventCreatedOn": 1487613810}
308 return event
309
James E. Blair8cce42e2016-10-18 08:18:36 -0700310 def getRefUpdatedEvent(self):
311 path = os.path.join(self.upstream_root, self.project)
312 repo = git.Repo(path)
313 oldrev = repo.heads[self.branch].commit.hexsha
314
315 event = {
316 "type": "ref-updated",
317 "submitter": {
318 "name": "User Name",
319 },
320 "refUpdate": {
321 "oldRev": oldrev,
322 "newRev": self.patchsets[-1]['revision'],
323 "refName": self.branch,
324 "project": self.project,
325 }
326 }
327 return event
328
Joshua Hesketh642824b2014-07-01 17:54:59 +1000329 def addApproval(self, category, value, username='reviewer_john',
330 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700331 if not granted_on:
332 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000333 approval = {
334 'description': self.categories[category][0],
335 'type': category,
336 'value': str(value),
337 'by': {
338 'username': username,
339 'email': username + '@example.com',
340 },
341 'grantedOn': int(granted_on)
342 }
Clark Boylanb640e052014-04-03 16:41:46 -0700343 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
344 if x['by']['username'] == username and x['type'] == category:
345 del self.patchsets[-1]['approvals'][i]
346 self.patchsets[-1]['approvals'].append(approval)
347 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000348 'author': {'email': 'author@example.com',
349 'name': 'Patchset Author',
350 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700351 'change': {'branch': self.branch,
352 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
353 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000354 'owner': {'email': 'owner@example.com',
355 'name': 'Change Owner',
356 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700357 'project': self.project,
358 'subject': self.subject,
359 'topic': 'master',
360 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700362 'patchSet': self.patchsets[-1],
363 'type': 'comment-added'}
364 self.data['submitRecords'] = self.getSubmitRecords()
365 return json.loads(json.dumps(event))
366
367 def getSubmitRecords(self):
368 status = {}
369 for cat in self.categories.keys():
370 status[cat] = 0
371
372 for a in self.patchsets[-1]['approvals']:
373 cur = status[a['type']]
374 cat_min, cat_max = self.categories[a['type']][1:]
375 new = int(a['value'])
376 if new == cat_min:
377 cur = new
378 elif abs(new) > abs(cur):
379 cur = new
380 status[a['type']] = cur
381
382 labels = []
383 ok = True
384 for typ, cat in self.categories.items():
385 cur = status[typ]
386 cat_min, cat_max = cat[1:]
387 if cur == cat_min:
388 value = 'REJECT'
389 ok = False
390 elif cur == cat_max:
391 value = 'OK'
392 else:
393 value = 'NEED'
394 ok = False
395 labels.append({'label': cat[0], 'status': value})
396 if ok:
397 return [{'status': 'OK'}]
398 return [{'status': 'NOT_READY',
399 'labels': labels}]
400
401 def setDependsOn(self, other, patchset):
402 self.depends_on_change = other
403 d = {'id': other.data['id'],
404 'number': other.data['number'],
405 'ref': other.patchsets[patchset - 1]['ref']
406 }
407 self.data['dependsOn'] = [d]
408
409 other.needed_by_changes.append(self)
410 needed = other.data.get('neededBy', [])
411 d = {'id': self.data['id'],
412 'number': self.data['number'],
413 'ref': self.patchsets[patchset - 1]['ref'],
414 'revision': self.patchsets[patchset - 1]['revision']
415 }
416 needed.append(d)
417 other.data['neededBy'] = needed
418
419 def query(self):
420 self.queried += 1
421 d = self.data.get('dependsOn')
422 if d:
423 d = d[0]
424 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
425 d['isCurrentPatchSet'] = True
426 else:
427 d['isCurrentPatchSet'] = False
428 return json.loads(json.dumps(self.data))
429
430 def setMerged(self):
431 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000432 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700433 return
434 if self.fail_merge:
435 return
436 self.data['status'] = 'MERGED'
437 self.open = False
438
439 path = os.path.join(self.upstream_root, self.project)
440 repo = git.Repo(path)
441 repo.heads[self.branch].commit = \
442 repo.commit(self.patchsets[-1]['revision'])
443
444 def setReported(self):
445 self.reported += 1
446
447
James E. Blaire511d2f2016-12-08 15:22:26 -0800448class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700449 """A Fake Gerrit connection for use in tests.
450
451 This subclasses
452 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
453 ability for tests to add changes to the fake Gerrit it represents.
454 """
455
Joshua Hesketh352264b2015-08-11 23:42:08 +1000456 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700459 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800460 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000461 connection_config)
462
Monty Taylorb934c1a2017-06-16 19:31:47 -0500463 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700464 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
465 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700467 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200468 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700469
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700470 def addFakeChange(self, project, branch, subject, status='NEW',
471 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700472 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700474 c = FakeGerritChange(self, self.change_number, project, branch,
475 subject, upstream_root=self.upstream_root,
476 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.changes[self.change_number] = c
478 return c
479
Clark Boylanb640e052014-04-03 16:41:46 -0700480 def review(self, project, changeid, message, action):
481 number, ps = changeid.split(',')
482 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000483
484 # Add the approval back onto the change (ie simulate what gerrit would
485 # do).
486 # Usually when zuul leaves a review it'll create a feedback loop where
487 # zuul's review enters another gerrit event (which is then picked up by
488 # zuul). However, we can't mimic this behaviour (by adding this
489 # approval event into the queue) as it stops jobs from checking what
490 # happens before this event is triggered. If a job needs to see what
491 # happens they can add their own verified event into the queue.
492 # Nevertheless, we can update change with the new review in gerrit.
493
James E. Blair8b5408c2016-08-08 15:37:46 -0700494 for cat in action.keys():
495 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000496 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000497
Clark Boylanb640e052014-04-03 16:41:46 -0700498 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000499
Clark Boylanb640e052014-04-03 16:41:46 -0700500 if 'submit' in action:
501 change.setMerged()
502 if message:
503 change.setReported()
504
505 def query(self, number):
506 change = self.changes.get(int(number))
507 if change:
508 return change.query()
509 return {}
510
James E. Blairc494d542014-08-06 09:23:52 -0700511 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700512 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700513 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800514 if query.startswith('change:'):
515 # Query a specific changeid
516 changeid = query[len('change:'):]
517 l = [change.query() for change in self.changes.values()
518 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700519 elif query.startswith('message:'):
520 # Query the content of a commit message
521 msg = query[len('message:'):].strip()
522 l = [change.query() for change in self.changes.values()
523 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800524 else:
525 # Query all open changes
526 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700527 return l
James E. Blairc494d542014-08-06 09:23:52 -0700528
Joshua Hesketh352264b2015-08-11 23:42:08 +1000529 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700530 pass
531
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200532 def _uploadPack(self, project):
533 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
534 'multi_ack thin-pack side-band side-band-64k ofs-delta '
535 'shallow no-progress include-tag multi_ack_detailed no-done\n')
536 path = os.path.join(self.upstream_root, project.name)
537 repo = git.Repo(path)
538 for ref in repo.refs:
539 r = ref.object.hexsha + ' ' + ref.path + '\n'
540 ret += '%04x%s' % (len(r) + 4, r)
541 ret += '0000'
542 return ret
543
Joshua Hesketh352264b2015-08-11 23:42:08 +1000544 def getGitUrl(self, project):
545 return os.path.join(self.upstream_root, project.name)
546
Clark Boylanb640e052014-04-03 16:41:46 -0700547
Gregory Haynes4fc12542015-04-22 20:38:06 -0700548class GithubChangeReference(git.Reference):
549 _common_path_default = "refs/pull"
550 _points_to_commits_only = True
551
552
553class FakeGithubPullRequest(object):
554
555 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800556 subject, upstream_root, files=[], number_of_commits=1,
557 writers=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700558 """Creates a new PR with several commits.
559 Sends an event about opened PR."""
560 self.github = github
561 self.source = github
562 self.number = number
563 self.project = project
564 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100565 self.subject = subject
566 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700567 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100570 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100571 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800572 self.reviews = []
573 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 self.updated_at = None
575 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100576 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100577 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700578 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700579 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100580 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700581 self._updateTimeStamp()
582
Jan Hruban570d01c2016-03-10 21:51:32 +0100583 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100585 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700586 self._updateTimeStamp()
587
Jan Hruban570d01c2016-03-10 21:51:32 +0100588 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700589 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100590 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700591 self._updateTimeStamp()
592
593 def getPullRequestOpenedEvent(self):
594 return self._getPullRequestEvent('opened')
595
596 def getPullRequestSynchronizeEvent(self):
597 return self._getPullRequestEvent('synchronize')
598
599 def getPullRequestReopenedEvent(self):
600 return self._getPullRequestEvent('reopened')
601
602 def getPullRequestClosedEvent(self):
603 return self._getPullRequestEvent('closed')
604
605 def addComment(self, message):
606 self.comments.append(message)
607 self._updateTimeStamp()
608
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200609 def getCommentAddedEvent(self, text):
610 name = 'issue_comment'
611 data = {
612 'action': 'created',
613 'issue': {
614 'number': self.number
615 },
616 'comment': {
617 'body': text
618 },
619 'repository': {
620 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100621 },
622 'sender': {
623 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200624 }
625 }
626 return (name, data)
627
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800628 def getReviewAddedEvent(self, review):
629 name = 'pull_request_review'
630 data = {
631 'action': 'submitted',
632 'pull_request': {
633 'number': self.number,
634 'title': self.subject,
635 'updated_at': self.updated_at,
636 'base': {
637 'ref': self.branch,
638 'repo': {
639 'full_name': self.project
640 }
641 },
642 'head': {
643 'sha': self.head_sha
644 }
645 },
646 'review': {
647 'state': review
648 },
649 'repository': {
650 'full_name': self.project
651 },
652 'sender': {
653 'login': 'ghuser'
654 }
655 }
656 return (name, data)
657
Jan Hruban16ad31f2015-11-07 14:39:07 +0100658 def addLabel(self, name):
659 if name not in self.labels:
660 self.labels.append(name)
661 self._updateTimeStamp()
662 return self._getLabelEvent(name)
663
664 def removeLabel(self, name):
665 if name in self.labels:
666 self.labels.remove(name)
667 self._updateTimeStamp()
668 return self._getUnlabelEvent(name)
669
670 def _getLabelEvent(self, label):
671 name = 'pull_request'
672 data = {
673 'action': 'labeled',
674 'pull_request': {
675 'number': self.number,
676 'updated_at': self.updated_at,
677 'base': {
678 'ref': self.branch,
679 'repo': {
680 'full_name': self.project
681 }
682 },
683 'head': {
684 'sha': self.head_sha
685 }
686 },
687 'label': {
688 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100689 },
690 'sender': {
691 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100692 }
693 }
694 return (name, data)
695
696 def _getUnlabelEvent(self, label):
697 name = 'pull_request'
698 data = {
699 'action': 'unlabeled',
700 'pull_request': {
701 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100702 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100703 'updated_at': self.updated_at,
704 'base': {
705 'ref': self.branch,
706 'repo': {
707 'full_name': self.project
708 }
709 },
710 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800711 'sha': self.head_sha,
712 'repo': {
713 'full_name': self.project
714 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100715 }
716 },
717 'label': {
718 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100719 },
720 'sender': {
721 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100722 }
723 }
724 return (name, data)
725
Gregory Haynes4fc12542015-04-22 20:38:06 -0700726 def _getRepo(self):
727 repo_path = os.path.join(self.upstream_root, self.project)
728 return git.Repo(repo_path)
729
730 def _createPRRef(self):
731 repo = self._getRepo()
732 GithubChangeReference.create(
733 repo, self._getPRReference(), 'refs/tags/init')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700736 repo = self._getRepo()
737 ref = repo.references[self._getPRReference()]
738 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100739 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700740 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100741 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 repo.head.reference = ref
743 zuul.merger.merger.reset_repo_to_head(repo)
744 repo.git.clean('-x', '-f', '-d')
745
Jan Hruban570d01c2016-03-10 21:51:32 +0100746 if files:
747 fn = files[0]
748 self.files = files
749 else:
750 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
751 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100752 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700753 fn = os.path.join(repo.working_dir, fn)
754 f = open(fn, 'w')
755 with open(fn, 'w') as f:
756 f.write("test %s %s\n" %
757 (self.branch, self.number))
758 repo.index.add([fn])
759
760 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800761 # Create an empty set of statuses for the given sha,
762 # each sha on a PR may have a status set on it
763 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700764 repo.head.reference = 'master'
765 zuul.merger.merger.reset_repo_to_head(repo)
766 repo.git.clean('-x', '-f', '-d')
767 repo.heads['master'].checkout()
768
769 def _updateTimeStamp(self):
770 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
771
772 def getPRHeadSha(self):
773 repo = self._getRepo()
774 return repo.references[self._getPRReference()].commit.hexsha
775
Jesse Keatingae4cd272017-01-30 17:10:44 -0800776 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800777 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
778 # convert the timestamp to a str format that would be returned
779 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800780
Adam Gandelmand81dd762017-02-09 15:15:49 -0800781 if granted_on:
782 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
783 submitted_at = time.strftime(
784 gh_time_format, granted_on.timetuple())
785 else:
786 # github timestamps only down to the second, so we need to make
787 # sure reviews that tests add appear to be added over a period of
788 # time in the past and not all at once.
789 if not self.reviews:
790 # the first review happens 10 mins ago
791 offset = 600
792 else:
793 # subsequent reviews happen 1 minute closer to now
794 offset = 600 - (len(self.reviews) * 60)
795
796 granted_on = datetime.datetime.utcfromtimestamp(
797 time.time() - offset)
798 submitted_at = time.strftime(
799 gh_time_format, granted_on.timetuple())
800
Jesse Keatingae4cd272017-01-30 17:10:44 -0800801 self.reviews.append({
802 'state': state,
803 'user': {
804 'login': user,
805 'email': user + "@derp.com",
806 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800807 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800808 })
809
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 def _getPRReference(self):
811 return '%s/head' % self.number
812
813 def _getPullRequestEvent(self, action):
814 name = 'pull_request'
815 data = {
816 'action': action,
817 'number': self.number,
818 'pull_request': {
819 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100820 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700821 'updated_at': self.updated_at,
822 'base': {
823 'ref': self.branch,
824 'repo': {
825 'full_name': self.project
826 }
827 },
828 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800829 'sha': self.head_sha,
830 'repo': {
831 'full_name': self.project
832 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700833 }
Jan Hruban3b415922016-02-03 13:10:22 +0100834 },
835 'sender': {
836 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 }
838 }
839 return (name, data)
840
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800841 def getCommitStatusEvent(self, context, state='success', user='zuul'):
842 name = 'status'
843 data = {
844 'state': state,
845 'sha': self.head_sha,
846 'description': 'Test results for %s: %s' % (self.head_sha, state),
847 'target_url': 'http://zuul/%s' % self.head_sha,
848 'branches': [],
849 'context': context,
850 'sender': {
851 'login': user
852 }
853 }
854 return (name, data)
855
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856
857class FakeGithubConnection(githubconnection.GithubConnection):
858 log = logging.getLogger("zuul.test.FakeGithubConnection")
859
860 def __init__(self, driver, connection_name, connection_config,
861 upstream_root=None):
862 super(FakeGithubConnection, self).__init__(driver, connection_name,
863 connection_config)
864 self.connection_name = connection_name
865 self.pr_number = 0
866 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700867 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700868 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100869 self.merge_failure = False
870 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700871
Jan Hruban570d01c2016-03-10 21:51:32 +0100872 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700873 self.pr_number += 1
874 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100875 self, self.pr_number, project, branch, subject, self.upstream_root,
876 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700877 self.pull_requests.append(pull_request)
878 return pull_request
879
Jesse Keating71a47ff2017-06-06 11:36:43 -0700880 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
881 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700882 if not old_rev:
883 old_rev = '00000000000000000000000000000000'
884 if not new_rev:
885 new_rev = random_sha1()
886 name = 'push'
887 data = {
888 'ref': ref,
889 'before': old_rev,
890 'after': new_rev,
891 'repository': {
892 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700893 },
894 'commits': [
895 {
896 'added': added_files,
897 'removed': removed_files,
898 'modified': modified_files
899 }
900 ]
Wayne1a78c612015-06-11 17:14:13 -0700901 }
902 return (name, data)
903
Gregory Haynes4fc12542015-04-22 20:38:06 -0700904 def emitEvent(self, event):
905 """Emulates sending the GitHub webhook event to the connection."""
906 port = self.webapp.server.socket.getsockname()[1]
907 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700908 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700909 headers = {'X-Github-Event': name}
910 req = urllib.request.Request(
911 'http://localhost:%s/connection/%s/payload'
912 % (port, self.connection_name),
913 data=payload, headers=headers)
914 urllib.request.urlopen(req)
915
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200916 def getPull(self, project, number):
917 pr = self.pull_requests[number - 1]
918 data = {
919 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100920 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200921 'updated_at': pr.updated_at,
922 'base': {
923 'repo': {
924 'full_name': pr.project
925 },
926 'ref': pr.branch,
927 },
Jan Hruban37615e52015-11-19 14:30:49 +0100928 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -0700929 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200930 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800931 'sha': pr.head_sha,
932 'repo': {
933 'full_name': pr.project
934 }
Jesse Keating61040e72017-06-08 15:08:27 -0700935 },
936 'files': pr.files
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200937 }
938 return data
939
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800940 def getPullBySha(self, sha):
941 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
942 if len(prs) > 1:
943 raise Exception('Multiple pulls found with head sha: %s' % sha)
944 pr = prs[0]
945 return self.getPull(pr.project, pr.number)
946
Jesse Keatingae4cd272017-01-30 17:10:44 -0800947 def _getPullReviews(self, owner, project, number):
948 pr = self.pull_requests[number - 1]
949 return pr.reviews
950
Jan Hruban3b415922016-02-03 13:10:22 +0100951 def getUser(self, login):
952 data = {
953 'username': login,
954 'name': 'Github User',
955 'email': 'github.user@example.com'
956 }
957 return data
958
Jesse Keatingae4cd272017-01-30 17:10:44 -0800959 def getRepoPermission(self, project, login):
960 owner, proj = project.split('/')
961 for pr in self.pull_requests:
962 pr_owner, pr_project = pr.project.split('/')
963 if (pr_owner == owner and proj == pr_project):
964 if login in pr.writers:
965 return 'write'
966 else:
967 return 'read'
968
Gregory Haynes4fc12542015-04-22 20:38:06 -0700969 def getGitUrl(self, project):
970 return os.path.join(self.upstream_root, str(project))
971
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200972 def real_getGitUrl(self, project):
973 return super(FakeGithubConnection, self).getGitUrl(project)
974
Gregory Haynes4fc12542015-04-22 20:38:06 -0700975 def getProjectBranches(self, project):
976 """Masks getProjectBranches since we don't have a real github"""
977
978 # just returns master for now
979 return ['master']
980
Jan Hrubane252a732017-01-03 15:03:09 +0100981 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700982 pull_request = self.pull_requests[pr_number - 1]
983 pull_request.addComment(message)
984
Jan Hruban3b415922016-02-03 13:10:22 +0100985 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100986 pull_request = self.pull_requests[pr_number - 1]
987 if self.merge_failure:
988 raise Exception('Pull request was not merged')
989 if self.merge_not_allowed_count > 0:
990 self.merge_not_allowed_count -= 1
991 raise MergeFailure('Merge was not successful due to mergeability'
992 ' conflict')
993 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100994 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100995
Jesse Keatingd96e5882017-01-19 13:55:50 -0800996 def getCommitStatuses(self, project, sha):
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700997 return self.statuses.get(project, {}).get(sha, [])
Jesse Keatingd96e5882017-01-19 13:55:50 -0800998
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700999 def setCommitStatus(self, project, sha, state, url='', description='',
1000 context='default', user='zuul'):
1001 # always insert a status to the front of the list, to represent
1002 # the last status provided for a commit.
1003 # Since we're bypassing github API, which would require a user, we
1004 # default the user as 'zuul' here.
1005 self.statuses.setdefault(project, {}).setdefault(sha, [])
1006 self.statuses[project][sha].insert(0, {
1007 'state': state,
1008 'url': url,
1009 'description': description,
1010 'context': context,
1011 'creator': {
1012 'login': user
1013 }
1014 })
Jan Hrubane252a732017-01-03 15:03:09 +01001015
Jan Hruban16ad31f2015-11-07 14:39:07 +01001016 def labelPull(self, project, pr_number, label):
1017 pull_request = self.pull_requests[pr_number - 1]
1018 pull_request.addLabel(label)
1019
1020 def unlabelPull(self, project, pr_number, label):
1021 pull_request = self.pull_requests[pr_number - 1]
1022 pull_request.removeLabel(label)
1023
Gregory Haynes4fc12542015-04-22 20:38:06 -07001024
Clark Boylanb640e052014-04-03 16:41:46 -07001025class BuildHistory(object):
1026 def __init__(self, **kw):
1027 self.__dict__.update(kw)
1028
1029 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -07001030 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
1031 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -07001032
1033
Clark Boylanb640e052014-04-03 16:41:46 -07001034class FakeStatsd(threading.Thread):
1035 def __init__(self):
1036 threading.Thread.__init__(self)
1037 self.daemon = True
1038 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1039 self.sock.bind(('', 0))
1040 self.port = self.sock.getsockname()[1]
1041 self.wake_read, self.wake_write = os.pipe()
1042 self.stats = []
1043
1044 def run(self):
1045 while True:
1046 poll = select.poll()
1047 poll.register(self.sock, select.POLLIN)
1048 poll.register(self.wake_read, select.POLLIN)
1049 ret = poll.poll()
1050 for (fd, event) in ret:
1051 if fd == self.sock.fileno():
1052 data = self.sock.recvfrom(1024)
1053 if not data:
1054 return
1055 self.stats.append(data[0])
1056 if fd == self.wake_read:
1057 return
1058
1059 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001060 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001061
1062
James E. Blaire1767bc2016-08-02 10:00:27 -07001063class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001064 log = logging.getLogger("zuul.test")
1065
Paul Belanger174a8272017-03-14 13:20:10 -04001066 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001067 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001068 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001069 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001070 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001071 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001072 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001073 # TODOv3(jeblair): self.node is really "the label of the node
1074 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001075 # keep using it like this, or we may end up exposing more of
1076 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001077 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001078 self.node = None
1079 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001080 self.node = self.parameters['nodes'][0]['label']
Clark Boylanb640e052014-04-03 16:41:46 -07001081 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001082 self.pipeline = self.parameters['ZUUL_PIPELINE']
1083 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001084 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001085 self.wait_condition = threading.Condition()
1086 self.waiting = False
1087 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001088 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001089 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001090 self.changes = None
1091 if 'ZUUL_CHANGE_IDS' in self.parameters:
1092 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001093
James E. Blair3158e282016-08-19 09:34:11 -07001094 def __repr__(self):
1095 waiting = ''
1096 if self.waiting:
1097 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001098 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1099 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001100
Clark Boylanb640e052014-04-03 16:41:46 -07001101 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001102 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001103 self.wait_condition.acquire()
1104 self.wait_condition.notify()
1105 self.waiting = False
1106 self.log.debug("Build %s released" % self.unique)
1107 self.wait_condition.release()
1108
1109 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001110 """Return whether this build is being held.
1111
1112 :returns: Whether the build is being held.
1113 :rtype: bool
1114 """
1115
Clark Boylanb640e052014-04-03 16:41:46 -07001116 self.wait_condition.acquire()
1117 if self.waiting:
1118 ret = True
1119 else:
1120 ret = False
1121 self.wait_condition.release()
1122 return ret
1123
1124 def _wait(self):
1125 self.wait_condition.acquire()
1126 self.waiting = True
1127 self.log.debug("Build %s waiting" % self.unique)
1128 self.wait_condition.wait()
1129 self.wait_condition.release()
1130
1131 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001132 self.log.debug('Running build %s' % self.unique)
1133
Paul Belanger174a8272017-03-14 13:20:10 -04001134 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001135 self.log.debug('Holding build %s' % self.unique)
1136 self._wait()
1137 self.log.debug("Build %s continuing" % self.unique)
1138
James E. Blair412fba82017-01-26 15:00:50 -08001139 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001140 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001141 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001142 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001143 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001144 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001145 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001146
James E. Blaire1767bc2016-08-02 10:00:27 -07001147 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001148
James E. Blaira5dba232016-08-08 15:53:24 -07001149 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001150 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001151 for change in changes:
1152 if self.hasChanges(change):
1153 return True
1154 return False
1155
James E. Blaire7b99a02016-08-05 14:27:34 -07001156 def hasChanges(self, *changes):
1157 """Return whether this build has certain changes in its git repos.
1158
1159 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001160 are expected to be present (in order) in the git repository of
1161 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001162
1163 :returns: Whether the build has the indicated changes.
1164 :rtype: bool
1165
1166 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001167 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001168 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001169 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001170 try:
1171 repo = git.Repo(path)
1172 except NoSuchPathError as e:
1173 self.log.debug('%s' % e)
1174 return False
1175 ref = self.parameters['ZUUL_REF']
1176 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1177 commit_message = '%s-1' % change.subject
1178 self.log.debug("Checking if build %s has changes; commit_message "
1179 "%s; repo_messages %s" % (self, commit_message,
1180 repo_messages))
1181 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001182 self.log.debug(" messages do not match")
1183 return False
1184 self.log.debug(" OK")
1185 return True
1186
James E. Blaird8af5422017-05-24 13:59:40 -07001187 def getWorkspaceRepos(self, projects):
1188 """Return workspace git repo objects for the listed projects
1189
1190 :arg list projects: A list of strings, each the canonical name
1191 of a project.
1192
1193 :returns: A dictionary of {name: repo} for every listed
1194 project.
1195 :rtype: dict
1196
1197 """
1198
1199 repos = {}
1200 for project in projects:
1201 path = os.path.join(self.jobdir.src_root, project)
1202 repo = git.Repo(path)
1203 repos[project] = repo
1204 return repos
1205
Clark Boylanb640e052014-04-03 16:41:46 -07001206
Paul Belanger174a8272017-03-14 13:20:10 -04001207class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1208 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001209
Paul Belanger174a8272017-03-14 13:20:10 -04001210 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001211 they will report that they have started but then pause until
1212 released before reporting completion. This attribute may be
1213 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001214 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001215 be explicitly released.
1216
1217 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001218 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001219 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001220 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001221 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001222 self.hold_jobs_in_build = False
1223 self.lock = threading.Lock()
1224 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001225 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001226 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001227 self.job_builds = {}
Monty Taylorde8242c2017-02-23 20:29:53 -06001228 self.hostname = 'zl.example.com'
James E. Blairf5dbd002015-12-23 15:26:17 -08001229
James E. Blaira5dba232016-08-08 15:53:24 -07001230 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001231 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001232
1233 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001234 :arg Change change: The :py:class:`~tests.base.FakeChange`
1235 instance which should cause the job to fail. This job
1236 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001237
1238 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001239 l = self.fail_tests.get(name, [])
1240 l.append(change)
1241 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001242
James E. Blair962220f2016-08-03 11:22:38 -07001243 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001244 """Release a held build.
1245
1246 :arg str regex: A regular expression which, if supplied, will
1247 cause only builds with matching names to be released. If
1248 not supplied, all builds will be released.
1249
1250 """
James E. Blair962220f2016-08-03 11:22:38 -07001251 builds = self.running_builds[:]
1252 self.log.debug("Releasing build %s (%s)" % (regex,
1253 len(self.running_builds)))
1254 for build in builds:
1255 if not regex or re.match(regex, build.name):
1256 self.log.debug("Releasing build %s" %
1257 (build.parameters['ZUUL_UUID']))
1258 build.release()
1259 else:
1260 self.log.debug("Not releasing build %s" %
1261 (build.parameters['ZUUL_UUID']))
1262 self.log.debug("Done releasing builds %s (%s)" %
1263 (regex, len(self.running_builds)))
1264
Paul Belanger174a8272017-03-14 13:20:10 -04001265 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001266 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001267 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001268 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001269 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001270 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001271 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001272 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001273 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1274 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001275
1276 def stopJob(self, job):
1277 self.log.debug("handle stop")
1278 parameters = json.loads(job.arguments)
1279 uuid = parameters['uuid']
1280 for build in self.running_builds:
1281 if build.unique == uuid:
1282 build.aborted = True
1283 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001284 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001285
James E. Blaira002b032017-04-18 10:35:48 -07001286 def stop(self):
1287 for build in self.running_builds:
1288 build.release()
1289 super(RecordingExecutorServer, self).stop()
1290
Joshua Hesketh50c21782016-10-13 21:34:14 +11001291
Paul Belanger174a8272017-03-14 13:20:10 -04001292class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
James E. Blairf327c572017-05-24 13:58:42 -07001293 def doMergeChanges(self, merger, items, repo_state):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001294 # Get a merger in order to update the repos involved in this job.
James E. Blair1960d682017-04-28 15:44:14 -07001295 commit = super(RecordingAnsibleJob, self).doMergeChanges(
James E. Blairf327c572017-05-24 13:58:42 -07001296 merger, items, repo_state)
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001297 if not commit: # merge conflict
1298 self.recordResult('MERGER_FAILURE')
1299 return commit
1300
1301 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001302 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001303 self.executor_server.lock.acquire()
1304 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001305 BuildHistory(name=build.name, result=result, changes=build.changes,
1306 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001307 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001308 pipeline=build.parameters['ZUUL_PIPELINE'])
1309 )
Paul Belanger174a8272017-03-14 13:20:10 -04001310 self.executor_server.running_builds.remove(build)
1311 del self.executor_server.job_builds[self.job.unique]
1312 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001313
1314 def runPlaybooks(self, args):
1315 build = self.executor_server.job_builds[self.job.unique]
1316 build.jobdir = self.jobdir
1317
1318 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1319 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001320 return result
1321
Monty Taylore6562aa2017-02-20 07:37:39 -05001322 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001323 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001324
Paul Belanger174a8272017-03-14 13:20:10 -04001325 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001326 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001327 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001328 else:
1329 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001330 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001331
James E. Blairad8dca02017-02-21 11:48:32 -05001332 def getHostList(self, args):
1333 self.log.debug("hostlist")
1334 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001335 for host in hosts:
1336 host['host_vars']['ansible_connection'] = 'local'
1337
1338 hosts.append(dict(
1339 name='localhost',
1340 host_vars=dict(ansible_connection='local'),
1341 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001342 return hosts
1343
James E. Blairf5dbd002015-12-23 15:26:17 -08001344
Clark Boylanb640e052014-04-03 16:41:46 -07001345class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001346 """A Gearman server for use in tests.
1347
1348 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1349 added to the queue but will not be distributed to workers
1350 until released. This attribute may be changed at any time and
1351 will take effect for subsequently enqueued jobs, but
1352 previously held jobs will still need to be explicitly
1353 released.
1354
1355 """
1356
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001357 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001358 self.hold_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001359 if use_ssl:
1360 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1361 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1362 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1363 else:
1364 ssl_ca = None
1365 ssl_cert = None
1366 ssl_key = None
1367
1368 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1369 ssl_cert=ssl_cert,
1370 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001371
1372 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001373 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1374 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001375 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001376 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001377 job.waiting = self.hold_jobs_in_queue
1378 else:
1379 job.waiting = False
1380 if job.waiting:
1381 continue
1382 if job.name in connection.functions:
1383 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001384 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001385 connection.related_jobs[job.handle] = job
1386 job.worker_connection = connection
1387 job.running = True
1388 return job
1389 return None
1390
1391 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001392 """Release a held job.
1393
1394 :arg str regex: A regular expression which, if supplied, will
1395 cause only jobs with matching names to be released. If
1396 not supplied, all jobs will be released.
1397 """
Clark Boylanb640e052014-04-03 16:41:46 -07001398 released = False
1399 qlen = (len(self.high_queue) + len(self.normal_queue) +
1400 len(self.low_queue))
1401 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1402 for job in self.getQueue():
Clint Byrum03454a52017-05-26 17:14:02 -07001403 if job.name != b'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001404 continue
Clint Byrum03454a52017-05-26 17:14:02 -07001405 parameters = json.loads(job.arguments.decode('utf8'))
Paul Belanger6ab6af72016-11-06 11:32:59 -05001406 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001407 self.log.debug("releasing queued job %s" %
1408 job.unique)
1409 job.waiting = False
1410 released = True
1411 else:
1412 self.log.debug("not releasing queued job %s" %
1413 job.unique)
1414 if released:
1415 self.wakeConnections()
1416 qlen = (len(self.high_queue) + len(self.normal_queue) +
1417 len(self.low_queue))
1418 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1419
1420
1421class FakeSMTP(object):
1422 log = logging.getLogger('zuul.FakeSMTP')
1423
1424 def __init__(self, messages, server, port):
1425 self.server = server
1426 self.port = port
1427 self.messages = messages
1428
1429 def sendmail(self, from_email, to_email, msg):
1430 self.log.info("Sending email from %s, to %s, with msg %s" % (
1431 from_email, to_email, msg))
1432
1433 headers = msg.split('\n\n', 1)[0]
1434 body = msg.split('\n\n', 1)[1]
1435
1436 self.messages.append(dict(
1437 from_email=from_email,
1438 to_email=to_email,
1439 msg=msg,
1440 headers=headers,
1441 body=body,
1442 ))
1443
1444 return True
1445
1446 def quit(self):
1447 return True
1448
1449
James E. Blairdce6cea2016-12-20 16:45:32 -08001450class FakeNodepool(object):
1451 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001452 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001453
1454 log = logging.getLogger("zuul.test.FakeNodepool")
1455
1456 def __init__(self, host, port, chroot):
1457 self.client = kazoo.client.KazooClient(
1458 hosts='%s:%s%s' % (host, port, chroot))
1459 self.client.start()
1460 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001461 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001462 self.thread = threading.Thread(target=self.run)
1463 self.thread.daemon = True
1464 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001465 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001466
1467 def stop(self):
1468 self._running = False
1469 self.thread.join()
1470 self.client.stop()
1471 self.client.close()
1472
1473 def run(self):
1474 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001475 try:
1476 self._run()
1477 except Exception:
1478 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001479 time.sleep(0.1)
1480
1481 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001482 if self.paused:
1483 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001484 for req in self.getNodeRequests():
1485 self.fulfillRequest(req)
1486
1487 def getNodeRequests(self):
1488 try:
1489 reqids = self.client.get_children(self.REQUEST_ROOT)
1490 except kazoo.exceptions.NoNodeError:
1491 return []
1492 reqs = []
1493 for oid in sorted(reqids):
1494 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001495 try:
1496 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001497 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001498 data['_oid'] = oid
1499 reqs.append(data)
1500 except kazoo.exceptions.NoNodeError:
1501 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001502 return reqs
1503
James E. Blaire18d4602017-01-05 11:17:28 -08001504 def getNodes(self):
1505 try:
1506 nodeids = self.client.get_children(self.NODE_ROOT)
1507 except kazoo.exceptions.NoNodeError:
1508 return []
1509 nodes = []
1510 for oid in sorted(nodeids):
1511 path = self.NODE_ROOT + '/' + oid
1512 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001513 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001514 data['_oid'] = oid
1515 try:
1516 lockfiles = self.client.get_children(path + '/lock')
1517 except kazoo.exceptions.NoNodeError:
1518 lockfiles = []
1519 if lockfiles:
1520 data['_lock'] = True
1521 else:
1522 data['_lock'] = False
1523 nodes.append(data)
1524 return nodes
1525
James E. Blaira38c28e2017-01-04 10:33:20 -08001526 def makeNode(self, request_id, node_type):
1527 now = time.time()
1528 path = '/nodepool/nodes/'
1529 data = dict(type=node_type,
1530 provider='test-provider',
1531 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001532 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001533 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001534 public_ipv4='127.0.0.1',
1535 private_ipv4=None,
1536 public_ipv6=None,
1537 allocated_to=request_id,
1538 state='ready',
1539 state_time=now,
1540 created_time=now,
1541 updated_time=now,
1542 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001543 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001544 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001545 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001546 path = self.client.create(path, data,
1547 makepath=True,
1548 sequence=True)
1549 nodeid = path.split("/")[-1]
1550 return nodeid
1551
James E. Blair6ab79e02017-01-06 10:10:17 -08001552 def addFailRequest(self, request):
1553 self.fail_requests.add(request['_oid'])
1554
James E. Blairdce6cea2016-12-20 16:45:32 -08001555 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001556 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001557 return
1558 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001559 oid = request['_oid']
1560 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001561
James E. Blair6ab79e02017-01-06 10:10:17 -08001562 if oid in self.fail_requests:
1563 request['state'] = 'failed'
1564 else:
1565 request['state'] = 'fulfilled'
1566 nodes = []
1567 for node in request['node_types']:
1568 nodeid = self.makeNode(oid, node)
1569 nodes.append(nodeid)
1570 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001571
James E. Blaira38c28e2017-01-04 10:33:20 -08001572 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001573 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001574 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001575 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001576 try:
1577 self.client.set(path, data)
1578 except kazoo.exceptions.NoNodeError:
1579 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001580
1581
James E. Blair498059b2016-12-20 13:50:13 -08001582class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001583 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001584 super(ChrootedKazooFixture, self).__init__()
1585
1586 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1587 if ':' in zk_host:
1588 host, port = zk_host.split(':')
1589 else:
1590 host = zk_host
1591 port = None
1592
1593 self.zookeeper_host = host
1594
1595 if not port:
1596 self.zookeeper_port = 2181
1597 else:
1598 self.zookeeper_port = int(port)
1599
Clark Boylan621ec9a2017-04-07 17:41:33 -07001600 self.test_id = test_id
1601
James E. Blair498059b2016-12-20 13:50:13 -08001602 def _setUp(self):
1603 # Make sure the test chroot paths do not conflict
1604 random_bits = ''.join(random.choice(string.ascii_lowercase +
1605 string.ascii_uppercase)
1606 for x in range(8))
1607
Clark Boylan621ec9a2017-04-07 17:41:33 -07001608 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001609 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1610
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001611 self.addCleanup(self._cleanup)
1612
James E. Blair498059b2016-12-20 13:50:13 -08001613 # Ensure the chroot path exists and clean up any pre-existing znodes.
1614 _tmp_client = kazoo.client.KazooClient(
1615 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1616 _tmp_client.start()
1617
1618 if _tmp_client.exists(self.zookeeper_chroot):
1619 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1620
1621 _tmp_client.ensure_path(self.zookeeper_chroot)
1622 _tmp_client.stop()
1623 _tmp_client.close()
1624
James E. Blair498059b2016-12-20 13:50:13 -08001625 def _cleanup(self):
1626 '''Remove the chroot path.'''
1627 # Need a non-chroot'ed client to remove the chroot path
1628 _tmp_client = kazoo.client.KazooClient(
1629 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1630 _tmp_client.start()
1631 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1632 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001633 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001634
1635
Joshua Heskethd78b4482015-09-14 16:56:34 -06001636class MySQLSchemaFixture(fixtures.Fixture):
1637 def setUp(self):
1638 super(MySQLSchemaFixture, self).setUp()
1639
1640 random_bits = ''.join(random.choice(string.ascii_lowercase +
1641 string.ascii_uppercase)
1642 for x in range(8))
1643 self.name = '%s_%s' % (random_bits, os.getpid())
1644 self.passwd = uuid.uuid4().hex
1645 db = pymysql.connect(host="localhost",
1646 user="openstack_citest",
1647 passwd="openstack_citest",
1648 db="openstack_citest")
1649 cur = db.cursor()
1650 cur.execute("create database %s" % self.name)
1651 cur.execute(
1652 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1653 (self.name, self.name, self.passwd))
1654 cur.execute("flush privileges")
1655
1656 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1657 self.passwd,
1658 self.name)
1659 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1660 self.addCleanup(self.cleanup)
1661
1662 def cleanup(self):
1663 db = pymysql.connect(host="localhost",
1664 user="openstack_citest",
1665 passwd="openstack_citest",
1666 db="openstack_citest")
1667 cur = db.cursor()
1668 cur.execute("drop database %s" % self.name)
1669 cur.execute("drop user '%s'@'localhost'" % self.name)
1670 cur.execute("flush privileges")
1671
1672
Maru Newby3fe5f852015-01-13 04:22:14 +00001673class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001674 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001675 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001676
James E. Blair1c236df2017-02-01 14:07:24 -08001677 def attachLogs(self, *args):
1678 def reader():
1679 self._log_stream.seek(0)
1680 while True:
1681 x = self._log_stream.read(4096)
1682 if not x:
1683 break
1684 yield x.encode('utf8')
1685 content = testtools.content.content_from_reader(
1686 reader,
1687 testtools.content_type.UTF8_TEXT,
1688 False)
1689 self.addDetail('logging', content)
1690
Clark Boylanb640e052014-04-03 16:41:46 -07001691 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001692 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001693 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1694 try:
1695 test_timeout = int(test_timeout)
1696 except ValueError:
1697 # If timeout value is invalid do not set a timeout.
1698 test_timeout = 0
1699 if test_timeout > 0:
1700 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1701
1702 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1703 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1704 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1705 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1706 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1707 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1708 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1709 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1710 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1711 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001712 self._log_stream = StringIO()
1713 self.addOnException(self.attachLogs)
1714 else:
1715 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001716
James E. Blair73b41772017-05-22 13:22:55 -07001717 # NOTE(jeblair): this is temporary extra debugging to try to
1718 # track down a possible leak.
1719 orig_git_repo_init = git.Repo.__init__
1720
1721 def git_repo_init(myself, *args, **kw):
1722 orig_git_repo_init(myself, *args, **kw)
1723 self.log.debug("Created git repo 0x%x %s" %
1724 (id(myself), repr(myself)))
1725
1726 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1727 git_repo_init))
1728
James E. Blair1c236df2017-02-01 14:07:24 -08001729 handler = logging.StreamHandler(self._log_stream)
1730 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1731 '%(levelname)-8s %(message)s')
1732 handler.setFormatter(formatter)
1733
1734 logger = logging.getLogger()
1735 logger.setLevel(logging.DEBUG)
1736 logger.addHandler(handler)
1737
Clark Boylan3410d532017-04-25 12:35:29 -07001738 # Make sure we don't carry old handlers around in process state
1739 # which slows down test runs
1740 self.addCleanup(logger.removeHandler, handler)
1741 self.addCleanup(handler.close)
1742 self.addCleanup(handler.flush)
1743
James E. Blair1c236df2017-02-01 14:07:24 -08001744 # NOTE(notmorgan): Extract logging overrides for specific
1745 # libraries from the OS_LOG_DEFAULTS env and create loggers
1746 # for each. This is used to limit the output during test runs
1747 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001748 log_defaults_from_env = os.environ.get(
1749 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001750 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001751
James E. Blairdce6cea2016-12-20 16:45:32 -08001752 if log_defaults_from_env:
1753 for default in log_defaults_from_env.split(','):
1754 try:
1755 name, level_str = default.split('=', 1)
1756 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001757 logger = logging.getLogger(name)
1758 logger.setLevel(level)
1759 logger.addHandler(handler)
1760 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001761 except ValueError:
1762 # NOTE(notmorgan): Invalid format of the log default,
1763 # skip and don't try and apply a logger for the
1764 # specified module
1765 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001766
Maru Newby3fe5f852015-01-13 04:22:14 +00001767
1768class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001769 """A test case with a functioning Zuul.
1770
1771 The following class variables are used during test setup and can
1772 be overidden by subclasses but are effectively read-only once a
1773 test method starts running:
1774
1775 :cvar str config_file: This points to the main zuul config file
1776 within the fixtures directory. Subclasses may override this
1777 to obtain a different behavior.
1778
1779 :cvar str tenant_config_file: This is the tenant config file
1780 (which specifies from what git repos the configuration should
1781 be loaded). It defaults to the value specified in
1782 `config_file` but can be overidden by subclasses to obtain a
1783 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001784 configuration. See also the :py:func:`simple_layout`
1785 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001786
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001787 :cvar bool create_project_keys: Indicates whether Zuul should
1788 auto-generate keys for each project, or whether the test
1789 infrastructure should insert dummy keys to save time during
1790 startup. Defaults to False.
1791
James E. Blaire7b99a02016-08-05 14:27:34 -07001792 The following are instance variables that are useful within test
1793 methods:
1794
1795 :ivar FakeGerritConnection fake_<connection>:
1796 A :py:class:`~tests.base.FakeGerritConnection` will be
1797 instantiated for each connection present in the config file
1798 and stored here. For instance, `fake_gerrit` will hold the
1799 FakeGerritConnection object for a connection named `gerrit`.
1800
1801 :ivar FakeGearmanServer gearman_server: An instance of
1802 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1803 server that all of the Zuul components in this test use to
1804 communicate with each other.
1805
Paul Belanger174a8272017-03-14 13:20:10 -04001806 :ivar RecordingExecutorServer executor_server: An instance of
1807 :py:class:`~tests.base.RecordingExecutorServer` which is the
1808 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001809
1810 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1811 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001812 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001813 list upon completion.
1814
1815 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1816 objects representing completed builds. They are appended to
1817 the list in the order they complete.
1818
1819 """
1820
James E. Blair83005782015-12-11 14:46:03 -08001821 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001822 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001823 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001824 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001825
1826 def _startMerger(self):
1827 self.merge_server = zuul.merger.server.MergeServer(self.config,
1828 self.connections)
1829 self.merge_server.start()
1830
Maru Newby3fe5f852015-01-13 04:22:14 +00001831 def setUp(self):
1832 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001833
1834 self.setupZK()
1835
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001836 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001837 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001838 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1839 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001840 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001841 tmp_root = tempfile.mkdtemp(
1842 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001843 self.test_root = os.path.join(tmp_root, "zuul-test")
1844 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001845 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001846 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001847 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001848
1849 if os.path.exists(self.test_root):
1850 shutil.rmtree(self.test_root)
1851 os.makedirs(self.test_root)
1852 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001853 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001854
1855 # Make per test copy of Configuration.
1856 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001857 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1858 if not os.path.exists(self.private_key_file):
1859 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1860 shutil.copy(src_private_key_file, self.private_key_file)
1861 shutil.copy('{}.pub'.format(src_private_key_file),
1862 '{}.pub'.format(self.private_key_file))
1863 os.chmod(self.private_key_file, 0o0600)
James E. Blair59fdbac2015-12-07 17:08:06 -08001864 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001865 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001866 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001867 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001868 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001869 self.config.set('zuul', 'state_dir', self.state_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001870 self.config.set('executor', 'private_key_file', self.private_key_file)
Clark Boylanb640e052014-04-03 16:41:46 -07001871
Clark Boylanb640e052014-04-03 16:41:46 -07001872 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001873 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1874 # see: https://github.com/jsocol/pystatsd/issues/61
1875 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001876 os.environ['STATSD_PORT'] = str(self.statsd.port)
1877 self.statsd.start()
1878 # the statsd client object is configured in the statsd module import
Monty Taylorb934c1a2017-06-16 19:31:47 -05001879 importlib.reload(statsd)
1880 importlib.reload(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001881
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001882 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001883
1884 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001885 self.log.info("Gearman server on port %s" %
1886 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001887 if self.use_ssl:
1888 self.log.info('SSL enabled for gearman')
1889 self.config.set(
1890 'gearman', 'ssl_ca',
1891 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1892 self.config.set(
1893 'gearman', 'ssl_cert',
1894 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1895 self.config.set(
1896 'gearman', 'ssl_key',
1897 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001898
James E. Blaire511d2f2016-12-08 15:22:26 -08001899 gerritsource.GerritSource.replication_timeout = 1.5
1900 gerritsource.GerritSource.replication_retry_interval = 0.5
1901 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001902
Joshua Hesketh352264b2015-08-11 23:42:08 +10001903 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001904
Jan Hruban7083edd2015-08-21 14:00:54 +02001905 self.webapp = zuul.webapp.WebApp(
1906 self.sched, port=0, listen_address='127.0.0.1')
1907
Jan Hruban6b71aff2015-10-22 16:58:08 +02001908 self.event_queues = [
1909 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001910 self.sched.trigger_event_queue,
1911 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001912 ]
1913
James E. Blairfef78942016-03-11 16:28:56 -08001914 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001915 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001916
Paul Belanger174a8272017-03-14 13:20:10 -04001917 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001918 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001919 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001920 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001921 _test_root=self.test_root,
1922 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001923 self.executor_server.start()
1924 self.history = self.executor_server.build_history
1925 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001926
Paul Belanger174a8272017-03-14 13:20:10 -04001927 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001928 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001929 self.merge_client = zuul.merger.client.MergeClient(
1930 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001931 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001932 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001933 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001934
James E. Blair0d5a36e2017-02-21 10:53:44 -05001935 self.fake_nodepool = FakeNodepool(
1936 self.zk_chroot_fixture.zookeeper_host,
1937 self.zk_chroot_fixture.zookeeper_port,
1938 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001939
Paul Belanger174a8272017-03-14 13:20:10 -04001940 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001941 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001942 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001943 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001944
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001945 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001946
1947 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001948 self.webapp.start()
1949 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001950 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001951 # Cleanups are run in reverse order
1952 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001953 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001954 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001955
James E. Blairb9c0d772017-03-03 14:34:49 -08001956 self.sched.reconfigure(self.config)
1957 self.sched.resume()
1958
Tobias Henkel7df274b2017-05-26 17:41:11 +02001959 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08001960 # Set up gerrit related fakes
1961 # Set a changes database so multiple FakeGerrit's can report back to
1962 # a virtual canonical database given by the configured hostname
1963 self.gerrit_changes_dbs = {}
1964
1965 def getGerritConnection(driver, name, config):
1966 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1967 con = FakeGerritConnection(driver, name, config,
1968 changes_db=db,
1969 upstream_root=self.upstream_root)
1970 self.event_queues.append(con.event_queue)
1971 setattr(self, 'fake_' + name, con)
1972 return con
1973
1974 self.useFixture(fixtures.MonkeyPatch(
1975 'zuul.driver.gerrit.GerritDriver.getConnection',
1976 getGerritConnection))
1977
Gregory Haynes4fc12542015-04-22 20:38:06 -07001978 def getGithubConnection(driver, name, config):
1979 con = FakeGithubConnection(driver, name, config,
1980 upstream_root=self.upstream_root)
1981 setattr(self, 'fake_' + name, con)
1982 return con
1983
1984 self.useFixture(fixtures.MonkeyPatch(
1985 'zuul.driver.github.GithubDriver.getConnection',
1986 getGithubConnection))
1987
James E. Blaire511d2f2016-12-08 15:22:26 -08001988 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001989 # TODO(jhesketh): This should come from lib.connections for better
1990 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001991 # Register connections from the config
1992 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001993
Joshua Hesketh352264b2015-08-11 23:42:08 +10001994 def FakeSMTPFactory(*args, **kw):
1995 args = [self.smtp_messages] + list(args)
1996 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001997
Joshua Hesketh352264b2015-08-11 23:42:08 +10001998 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001999
James E. Blaire511d2f2016-12-08 15:22:26 -08002000 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002001 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002002 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002003
James E. Blair83005782015-12-11 14:46:03 -08002004 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002005 # This creates the per-test configuration object. It can be
2006 # overriden by subclasses, but should not need to be since it
2007 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002008 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002009 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002010
2011 if not self.setupSimpleLayout():
2012 if hasattr(self, 'tenant_config_file'):
2013 self.config.set('zuul', 'tenant_config',
2014 self.tenant_config_file)
2015 git_path = os.path.join(
2016 os.path.dirname(
2017 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2018 'git')
2019 if os.path.exists(git_path):
2020 for reponame in os.listdir(git_path):
2021 project = reponame.replace('_', '/')
2022 self.copyDirToRepo(project,
2023 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002024 self.setupAllProjectKeys()
2025
James E. Blair06cc3922017-04-19 10:08:10 -07002026 def setupSimpleLayout(self):
2027 # If the test method has been decorated with a simple_layout,
2028 # use that instead of the class tenant_config_file. Set up a
2029 # single config-project with the specified layout, and
2030 # initialize repos for all of the 'project' entries which
2031 # appear in the layout.
2032 test_name = self.id().split('.')[-1]
2033 test = getattr(self, test_name)
2034 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002035 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002036 else:
2037 return False
2038
James E. Blairb70e55a2017-04-19 12:57:02 -07002039 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002040 path = os.path.join(FIXTURE_DIR, path)
2041 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002042 data = f.read()
2043 layout = yaml.safe_load(data)
2044 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002045 untrusted_projects = []
2046 for item in layout:
2047 if 'project' in item:
2048 name = item['project']['name']
2049 untrusted_projects.append(name)
2050 self.init_repo(name)
2051 self.addCommitToRepo(name, 'initial commit',
2052 files={'README': ''},
2053 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002054 if 'job' in item:
2055 jobname = item['job']['name']
2056 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002057
2058 root = os.path.join(self.test_root, "config")
2059 if not os.path.exists(root):
2060 os.makedirs(root)
2061 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2062 config = [{'tenant':
2063 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002064 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07002065 {'config-projects': ['common-config'],
2066 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002067 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002068 f.close()
2069 self.config.set('zuul', 'tenant_config',
2070 os.path.join(FIXTURE_DIR, f.name))
2071
2072 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07002073 self.addCommitToRepo('common-config', 'add content from fixture',
2074 files, branch='master', tag='init')
2075
2076 return True
2077
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002078 def setupAllProjectKeys(self):
2079 if self.create_project_keys:
2080 return
2081
2082 path = self.config.get('zuul', 'tenant_config')
2083 with open(os.path.join(FIXTURE_DIR, path)) as f:
2084 tenant_config = yaml.safe_load(f.read())
2085 for tenant in tenant_config:
2086 sources = tenant['tenant']['source']
2087 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002088 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002089 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002090 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002091 self.setupProjectKeys(source, project)
2092
2093 def setupProjectKeys(self, source, project):
2094 # Make sure we set up an RSA key for the project so that we
2095 # don't spend time generating one:
2096
2097 key_root = os.path.join(self.state_root, 'keys')
2098 if not os.path.isdir(key_root):
2099 os.mkdir(key_root, 0o700)
2100 private_key_file = os.path.join(key_root, source, project + '.pem')
2101 private_key_dir = os.path.dirname(private_key_file)
2102 self.log.debug("Installing test keys for project %s at %s" % (
2103 project, private_key_file))
2104 if not os.path.isdir(private_key_dir):
2105 os.makedirs(private_key_dir)
2106 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2107 with open(private_key_file, 'w') as o:
2108 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002109
James E. Blair498059b2016-12-20 13:50:13 -08002110 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002111 self.zk_chroot_fixture = self.useFixture(
2112 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002113 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002114 self.zk_chroot_fixture.zookeeper_host,
2115 self.zk_chroot_fixture.zookeeper_port,
2116 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002117
James E. Blair96c6bf82016-01-15 16:20:40 -08002118 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002119 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002120
2121 files = {}
2122 for (dirpath, dirnames, filenames) in os.walk(source_path):
2123 for filename in filenames:
2124 test_tree_filepath = os.path.join(dirpath, filename)
2125 common_path = os.path.commonprefix([test_tree_filepath,
2126 source_path])
2127 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2128 with open(test_tree_filepath, 'r') as f:
2129 content = f.read()
2130 files[relative_filepath] = content
2131 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002132 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002133
James E. Blaire18d4602017-01-05 11:17:28 -08002134 def assertNodepoolState(self):
2135 # Make sure that there are no pending requests
2136
2137 requests = self.fake_nodepool.getNodeRequests()
2138 self.assertEqual(len(requests), 0)
2139
2140 nodes = self.fake_nodepool.getNodes()
2141 for node in nodes:
2142 self.assertFalse(node['_lock'], "Node %s is locked" %
2143 (node['_oid'],))
2144
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002145 def assertNoGeneratedKeys(self):
2146 # Make sure that Zuul did not generate any project keys
2147 # (unless it was supposed to).
2148
2149 if self.create_project_keys:
2150 return
2151
2152 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2153 test_key = i.read()
2154
2155 key_root = os.path.join(self.state_root, 'keys')
2156 for root, dirname, files in os.walk(key_root):
2157 for fn in files:
2158 with open(os.path.join(root, fn)) as f:
2159 self.assertEqual(test_key, f.read())
2160
Clark Boylanb640e052014-04-03 16:41:46 -07002161 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002162 self.log.debug("Assert final state")
2163 # Make sure no jobs are running
2164 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002165 # Make sure that git.Repo objects have been garbage collected.
2166 repos = []
James E. Blair73b41772017-05-22 13:22:55 -07002167 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002168 gc.collect()
2169 for obj in gc.get_objects():
2170 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002171 self.log.debug("Leaked git repo object: 0x%x %s" %
2172 (id(obj), repr(obj)))
2173 for ref in gc.get_referrers(obj):
2174 self.log.debug(" Referrer %s" % (repr(ref)))
Clark Boylanb640e052014-04-03 16:41:46 -07002175 repos.append(obj)
James E. Blair73b41772017-05-22 13:22:55 -07002176 if repos:
2177 for obj in gc.garbage:
2178 self.log.debug(" Garbage %s" % (repr(obj)))
2179 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002180 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002181 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002182 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002183 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002184 for tenant in self.sched.abide.tenants.values():
2185 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002186 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002187 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002188
2189 def shutdown(self):
2190 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002191 self.executor_server.hold_jobs_in_build = False
2192 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002193 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002194 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002195 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002196 self.sched.stop()
2197 self.sched.join()
2198 self.statsd.stop()
2199 self.statsd.join()
2200 self.webapp.stop()
2201 self.webapp.join()
2202 self.rpc.stop()
2203 self.rpc.join()
2204 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002205 self.fake_nodepool.stop()
2206 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002207 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002208 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002209 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002210 # Further the pydevd threads also need to be whitelisted so debugging
2211 # e.g. in PyCharm is possible without breaking shutdown.
2212 whitelist = ['executor-watchdog',
2213 'pydevd.CommandThread',
2214 'pydevd.Reader',
2215 'pydevd.Writer',
2216 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002217 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002218 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002219 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002220 log_str = ""
2221 for thread_id, stack_frame in sys._current_frames().items():
2222 log_str += "Thread: %s\n" % thread_id
2223 log_str += "".join(traceback.format_stack(stack_frame))
2224 self.log.debug(log_str)
2225 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002226
James E. Blaira002b032017-04-18 10:35:48 -07002227 def assertCleanShutdown(self):
2228 pass
2229
James E. Blairc4ba97a2017-04-19 16:26:24 -07002230 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002231 parts = project.split('/')
2232 path = os.path.join(self.upstream_root, *parts[:-1])
2233 if not os.path.exists(path):
2234 os.makedirs(path)
2235 path = os.path.join(self.upstream_root, project)
2236 repo = git.Repo.init(path)
2237
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002238 with repo.config_writer() as config_writer:
2239 config_writer.set_value('user', 'email', 'user@example.com')
2240 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002241
Clark Boylanb640e052014-04-03 16:41:46 -07002242 repo.index.commit('initial commit')
2243 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002244 if tag:
2245 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002246
James E. Blair97d902e2014-08-21 13:25:56 -07002247 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002248 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002249 repo.git.clean('-x', '-f', '-d')
2250
James E. Blair97d902e2014-08-21 13:25:56 -07002251 def create_branch(self, project, branch):
2252 path = os.path.join(self.upstream_root, project)
2253 repo = git.Repo.init(path)
2254 fn = os.path.join(path, 'README')
2255
2256 branch_head = repo.create_head(branch)
2257 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002258 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002259 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002260 f.close()
2261 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002262 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002263
James E. Blair97d902e2014-08-21 13:25:56 -07002264 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002265 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002266 repo.git.clean('-x', '-f', '-d')
2267
Sachi King9f16d522016-03-16 12:20:45 +11002268 def create_commit(self, project):
2269 path = os.path.join(self.upstream_root, project)
2270 repo = git.Repo(path)
2271 repo.head.reference = repo.heads['master']
2272 file_name = os.path.join(path, 'README')
2273 with open(file_name, 'a') as f:
2274 f.write('creating fake commit\n')
2275 repo.index.add([file_name])
2276 commit = repo.index.commit('Creating a fake commit')
2277 return commit.hexsha
2278
James E. Blairf4a5f022017-04-18 14:01:10 -07002279 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002280 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002281 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002282 while len(self.builds):
2283 self.release(self.builds[0])
2284 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002285 i += 1
2286 if count is not None and i >= count:
2287 break
James E. Blairb8c16472015-05-05 14:55:26 -07002288
Clark Boylanb640e052014-04-03 16:41:46 -07002289 def release(self, job):
2290 if isinstance(job, FakeBuild):
2291 job.release()
2292 else:
2293 job.waiting = False
2294 self.log.debug("Queued job %s released" % job.unique)
2295 self.gearman_server.wakeConnections()
2296
2297 def getParameter(self, job, name):
2298 if isinstance(job, FakeBuild):
2299 return job.parameters[name]
2300 else:
2301 parameters = json.loads(job.arguments)
2302 return parameters[name]
2303
Clark Boylanb640e052014-04-03 16:41:46 -07002304 def haveAllBuildsReported(self):
2305 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002306 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002307 return False
2308 # Find out if every build that the worker has completed has been
2309 # reported back to Zuul. If it hasn't then that means a Gearman
2310 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002311 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002312 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002313 if not zbuild:
2314 # It has already been reported
2315 continue
2316 # It hasn't been reported yet.
2317 return False
2318 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002319 worker = self.executor_server.executor_worker
2320 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002321 if connection.state == 'GRAB_WAIT':
2322 return False
2323 return True
2324
2325 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002326 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002327 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002328 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002329 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002330 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002331 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002332 for j in conn.related_jobs.values():
2333 if j.unique == build.uuid:
2334 client_job = j
2335 break
2336 if not client_job:
2337 self.log.debug("%s is not known to the gearman client" %
2338 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002339 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002340 if not client_job.handle:
2341 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002342 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002343 server_job = self.gearman_server.jobs.get(client_job.handle)
2344 if not server_job:
2345 self.log.debug("%s is not known to the gearman server" %
2346 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002347 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002348 if not hasattr(server_job, 'waiting'):
2349 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002350 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002351 if server_job.waiting:
2352 continue
James E. Blair17302972016-08-10 16:11:42 -07002353 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002354 self.log.debug("%s has not reported start" % build)
2355 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002356 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002357 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002358 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002359 if worker_build:
2360 if worker_build.isWaiting():
2361 continue
2362 else:
2363 self.log.debug("%s is running" % worker_build)
2364 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002365 else:
James E. Blair962220f2016-08-03 11:22:38 -07002366 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002367 return False
James E. Blaira002b032017-04-18 10:35:48 -07002368 for (build_uuid, job_worker) in \
2369 self.executor_server.job_workers.items():
2370 if build_uuid not in seen_builds:
2371 self.log.debug("%s is not finalized" % build_uuid)
2372 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002373 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002374
James E. Blairdce6cea2016-12-20 16:45:32 -08002375 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002376 if self.fake_nodepool.paused:
2377 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002378 if self.sched.nodepool.requests:
2379 return False
2380 return True
2381
Jan Hruban6b71aff2015-10-22 16:58:08 +02002382 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002383 for event_queue in self.event_queues:
2384 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002385
2386 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002387 for event_queue in self.event_queues:
2388 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002389
Clark Boylanb640e052014-04-03 16:41:46 -07002390 def waitUntilSettled(self):
2391 self.log.debug("Waiting until settled...")
2392 start = time.time()
2393 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002394 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002395 self.log.error("Timeout waiting for Zuul to settle")
2396 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002397 for event_queue in self.event_queues:
2398 self.log.error(" %s: %s" %
2399 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002400 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002401 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002402 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002403 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002404 self.log.error("All requests completed: %s" %
2405 (self.areAllNodeRequestsComplete(),))
2406 self.log.error("Merge client jobs: %s" %
2407 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002408 raise Exception("Timeout waiting for Zuul to settle")
2409 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002410
Paul Belanger174a8272017-03-14 13:20:10 -04002411 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002412 # have all build states propogated to zuul?
2413 if self.haveAllBuildsReported():
2414 # Join ensures that the queue is empty _and_ events have been
2415 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002416 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002417 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002418 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002419 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002420 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002421 self.areAllNodeRequestsComplete() and
2422 all(self.eventQueuesEmpty())):
2423 # The queue empty check is placed at the end to
2424 # ensure that if a component adds an event between
2425 # when locked the run handler and checked that the
2426 # components were stable, we don't erroneously
2427 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002428 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002429 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002430 self.log.debug("...settled.")
2431 return
2432 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002433 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002434 self.sched.wake_event.wait(0.1)
2435
2436 def countJobResults(self, jobs, result):
2437 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002438 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002439
Monty Taylor0d926122017-05-24 08:07:56 -05002440 def getBuildByName(self, name):
2441 for build in self.builds:
2442 if build.name == name:
2443 return build
2444 raise Exception("Unable to find build %s" % name)
2445
James E. Blair96c6bf82016-01-15 16:20:40 -08002446 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002447 for job in self.history:
2448 if (job.name == name and
2449 (project is None or
2450 job.parameters['ZUUL_PROJECT'] == project)):
2451 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002452 raise Exception("Unable to find job %s in history" % name)
2453
2454 def assertEmptyQueues(self):
2455 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002456 for tenant in self.sched.abide.tenants.values():
2457 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002458 for pipeline_queue in pipeline.queues:
2459 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002460 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002461 pipeline.name, pipeline_queue.name,
2462 pipeline_queue.queue))
2463 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002464 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002465
2466 def assertReportedStat(self, key, value=None, kind=None):
2467 start = time.time()
2468 while time.time() < (start + 5):
2469 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002470 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002471 if key == k:
2472 if value is None and kind is None:
2473 return
2474 elif value:
2475 if value == v:
2476 return
2477 elif kind:
2478 if v.endswith('|' + kind):
2479 return
2480 time.sleep(0.1)
2481
Clark Boylanb640e052014-04-03 16:41:46 -07002482 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002483
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002484 def assertBuilds(self, builds):
2485 """Assert that the running builds are as described.
2486
2487 The list of running builds is examined and must match exactly
2488 the list of builds described by the input.
2489
2490 :arg list builds: A list of dictionaries. Each item in the
2491 list must match the corresponding build in the build
2492 history, and each element of the dictionary must match the
2493 corresponding attribute of the build.
2494
2495 """
James E. Blair3158e282016-08-19 09:34:11 -07002496 try:
2497 self.assertEqual(len(self.builds), len(builds))
2498 for i, d in enumerate(builds):
2499 for k, v in d.items():
2500 self.assertEqual(
2501 getattr(self.builds[i], k), v,
2502 "Element %i in builds does not match" % (i,))
2503 except Exception:
2504 for build in self.builds:
2505 self.log.error("Running build: %s" % build)
2506 else:
2507 self.log.error("No running builds")
2508 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002509
James E. Blairb536ecc2016-08-31 10:11:42 -07002510 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002511 """Assert that the completed builds are as described.
2512
2513 The list of completed builds is examined and must match
2514 exactly the list of builds described by the input.
2515
2516 :arg list history: A list of dictionaries. Each item in the
2517 list must match the corresponding build in the build
2518 history, and each element of the dictionary must match the
2519 corresponding attribute of the build.
2520
James E. Blairb536ecc2016-08-31 10:11:42 -07002521 :arg bool ordered: If true, the history must match the order
2522 supplied, if false, the builds are permitted to have
2523 arrived in any order.
2524
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002525 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002526 def matches(history_item, item):
2527 for k, v in item.items():
2528 if getattr(history_item, k) != v:
2529 return False
2530 return True
James E. Blair3158e282016-08-19 09:34:11 -07002531 try:
2532 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002533 if ordered:
2534 for i, d in enumerate(history):
2535 if not matches(self.history[i], d):
2536 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002537 "Element %i in history does not match %s" %
2538 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002539 else:
2540 unseen = self.history[:]
2541 for i, d in enumerate(history):
2542 found = False
2543 for unseen_item in unseen:
2544 if matches(unseen_item, d):
2545 found = True
2546 unseen.remove(unseen_item)
2547 break
2548 if not found:
2549 raise Exception("No match found for element %i "
2550 "in history" % (i,))
2551 if unseen:
2552 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002553 except Exception:
2554 for build in self.history:
2555 self.log.error("Completed build: %s" % build)
2556 else:
2557 self.log.error("No completed builds")
2558 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002559
James E. Blair6ac368c2016-12-22 18:07:20 -08002560 def printHistory(self):
2561 """Log the build history.
2562
2563 This can be useful during tests to summarize what jobs have
2564 completed.
2565
2566 """
2567 self.log.debug("Build history:")
2568 for build in self.history:
2569 self.log.debug(build)
2570
James E. Blair59fdbac2015-12-07 17:08:06 -08002571 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002572 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2573
James E. Blair9ea70072017-04-19 16:05:30 -07002574 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002575 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002576 if not os.path.exists(root):
2577 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002578 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2579 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002580- tenant:
2581 name: openstack
2582 source:
2583 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002584 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002585 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002586 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002587 - org/project
2588 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002589 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002590 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002591 self.config.set('zuul', 'tenant_config',
2592 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002593 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002594
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002595 def addCommitToRepo(self, project, message, files,
2596 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002597 path = os.path.join(self.upstream_root, project)
2598 repo = git.Repo(path)
2599 repo.head.reference = branch
2600 zuul.merger.merger.reset_repo_to_head(repo)
2601 for fn, content in files.items():
2602 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002603 try:
2604 os.makedirs(os.path.dirname(fn))
2605 except OSError:
2606 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002607 with open(fn, 'w') as f:
2608 f.write(content)
2609 repo.index.add([fn])
2610 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002611 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002612 repo.heads[branch].commit = commit
2613 repo.head.reference = branch
2614 repo.git.clean('-x', '-f', '-d')
2615 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002616 if tag:
2617 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002618 return before
2619
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002620 def commitConfigUpdate(self, project_name, source_name):
2621 """Commit an update to zuul.yaml
2622
2623 This overwrites the zuul.yaml in the specificed project with
2624 the contents specified.
2625
2626 :arg str project_name: The name of the project containing
2627 zuul.yaml (e.g., common-config)
2628
2629 :arg str source_name: The path to the file (underneath the
2630 test fixture directory) whose contents should be used to
2631 replace zuul.yaml.
2632 """
2633
2634 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002635 files = {}
2636 with open(source_path, 'r') as f:
2637 data = f.read()
2638 layout = yaml.safe_load(data)
2639 files['zuul.yaml'] = data
2640 for item in layout:
2641 if 'job' in item:
2642 jobname = item['job']['name']
2643 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002644 before = self.addCommitToRepo(
2645 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002646 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002647 return before
2648
James E. Blair7fc8daa2016-08-08 15:37:15 -07002649 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002650
James E. Blair7fc8daa2016-08-08 15:37:15 -07002651 """Inject a Fake (Gerrit) event.
2652
2653 This method accepts a JSON-encoded event and simulates Zuul
2654 having received it from Gerrit. It could (and should)
2655 eventually apply to any connection type, but is currently only
2656 used with Gerrit connections. The name of the connection is
2657 used to look up the corresponding server, and the event is
2658 simulated as having been received by all Zuul connections
2659 attached to that server. So if two Gerrit connections in Zuul
2660 are connected to the same Gerrit server, and you invoke this
2661 method specifying the name of one of them, the event will be
2662 received by both.
2663
2664 .. note::
2665
2666 "self.fake_gerrit.addEvent" calls should be migrated to
2667 this method.
2668
2669 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002670 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002671 :arg str event: The JSON-encoded event.
2672
2673 """
2674 specified_conn = self.connections.connections[connection]
2675 for conn in self.connections.connections.values():
2676 if (isinstance(conn, specified_conn.__class__) and
2677 specified_conn.server == conn.server):
2678 conn.addEvent(event)
2679
James E. Blaird8af5422017-05-24 13:59:40 -07002680 def getUpstreamRepos(self, projects):
2681 """Return upstream git repo objects for the listed projects
2682
2683 :arg list projects: A list of strings, each the canonical name
2684 of a project.
2685
2686 :returns: A dictionary of {name: repo} for every listed
2687 project.
2688 :rtype: dict
2689
2690 """
2691
2692 repos = {}
2693 for project in projects:
2694 # FIXME(jeblair): the upstream root does not yet have a
2695 # hostname component; that needs to be added, and this
2696 # line removed:
2697 tmp_project_name = '/'.join(project.split('/')[1:])
2698 path = os.path.join(self.upstream_root, tmp_project_name)
2699 repo = git.Repo(path)
2700 repos[project] = repo
2701 return repos
2702
James E. Blair3f876d52016-07-22 13:07:14 -07002703
2704class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002705 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002706 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002707
Joshua Heskethd78b4482015-09-14 16:56:34 -06002708
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002709class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002710 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002711 use_ssl = True
2712
2713
Joshua Heskethd78b4482015-09-14 16:56:34 -06002714class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002715 def setup_config(self):
2716 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002717 for section_name in self.config.sections():
2718 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2719 section_name, re.I)
2720 if not con_match:
2721 continue
2722
2723 if self.config.get(section_name, 'driver') == 'sql':
2724 f = MySQLSchemaFixture()
2725 self.useFixture(f)
2726 if (self.config.get(section_name, 'dburi') ==
2727 '$MYSQL_FIXTURE_DBURI$'):
2728 self.config.set(section_name, 'dburi', f.dburi)