blob: 1d3669405d47de023265918feda441a859f3be97 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Christian Berendtffba5df2014-06-07 21:30:22 +020018from six.moves import configparser as ConfigParser
Clark Boylanb640e052014-04-03 16:41:46 -070019import gc
20import hashlib
21import json
22import logging
23import os
Christian Berendt12d4d722014-06-07 21:03:45 +020024from six.moves import queue as Queue
Morgan Fainberg293f7f82016-05-30 14:01:22 -070025from six.moves import urllib
Clark Boylanb640e052014-04-03 16:41:46 -070026import random
27import re
28import select
29import shutil
Monty Taylor74fa3862016-06-02 07:39:49 +030030from six.moves import reload_module
Clark Boylan21a2c812017-04-24 15:44:55 -070031try:
32 from cStringIO import StringIO
33except Exception:
34 from six import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070035import socket
36import string
37import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080038import sys
James E. Blairf84026c2015-12-08 16:11:46 -080039import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070040import threading
Clark Boylan8208c192017-04-24 18:08:08 -070041import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070042import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060043import uuid
44
Clark Boylanb640e052014-04-03 16:41:46 -070045
46import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import statsd
53import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080054import testtools.content
55import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080056from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000057import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070058
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
63import zuul.webapp
64import zuul.rpclistener
Paul Belanger174a8272017-03-14 13:20:10 -040065import zuul.executor.server
66import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080067import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070068import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070069import zuul.merger.merger
70import zuul.merger.server
James E. Blair8d692392016-04-08 17:47:58 -070071import zuul.nodepool
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
Jan Hruban49bff072015-11-03 11:45:46 +010073from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070074
75FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
76 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080077
78KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070079
Clark Boylanb640e052014-04-03 16:41:46 -070080
81def repack_repo(path):
82 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
83 output = subprocess.Popen(cmd, close_fds=True,
84 stdout=subprocess.PIPE,
85 stderr=subprocess.PIPE)
86 out = output.communicate()
87 if output.returncode:
88 raise Exception("git repack returned %d" % output.returncode)
89 return out
90
91
92def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040093 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070094
95
James E. Blaira190f3b2015-01-05 14:56:54 -080096def iterate_timeout(max_seconds, purpose):
97 start = time.time()
98 count = 0
99 while (time.time() < start + max_seconds):
100 count += 1
101 yield count
102 time.sleep(0)
103 raise Exception("Timeout waiting for %s" % purpose)
104
105
Jesse Keating436a5452017-04-20 11:48:41 -0700106def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700107 """Specify a layout file for use by a test method.
108
109 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700110 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700111
112 Some tests require only a very simple configuration. For those,
113 establishing a complete config directory hierachy is too much
114 work. In those cases, you can add a simple zuul.yaml file to the
115 test fixtures directory (in fixtures/layouts/foo.yaml) and use
116 this decorator to indicate the test method should use that rather
117 than the tenant config file specified by the test class.
118
119 The decorator will cause that layout file to be added to a
120 config-project called "common-config" and each "project" instance
121 referenced in the layout file will have a git repo automatically
122 initialized.
123 """
124
125 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700126 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700127 return test
128 return decorator
129
130
Gregory Haynes4fc12542015-04-22 20:38:06 -0700131class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700132 _common_path_default = "refs/changes"
133 _points_to_commits_only = True
134
135
Gregory Haynes4fc12542015-04-22 20:38:06 -0700136class FakeGerritChange(object):
James E. Blair8b5408c2016-08-08 15:37:46 -0700137 categories = {'approved': ('Approved', -1, 1),
138 'code-review': ('Code-Review', -2, 2),
139 'verified': ('Verified', -2, 2)}
Clark Boylanb640e052014-04-03 16:41:46 -0700140
141 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700142 status='NEW', upstream_root=None, files={}):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
174 'url': 'https://hostname/%s' % number}
175
176 self.upstream_root = upstream_root
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700177 self.addPatchset(files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700181 def addFakeChangeToRepo(self, msg, files, large):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
186 'refs/tags/init')
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
195 with open(fn, 'w') as f:
196 f.write(content)
197 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700198 else:
199 for fni in range(100):
200 fn = os.path.join(path, str(fni))
201 f = open(fn, 'w')
202 for ci in range(4096):
203 f.write(random.choice(string.printable))
204 f.close()
205 repo.index.add([fn])
206
207 r = repo.index.commit(msg)
208 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700209 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700210 repo.git.clean('-x', '-f', '-d')
211 repo.heads['master'].checkout()
212 return r
213
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700214 def addPatchset(self, files=None, large=False):
Clark Boylanb640e052014-04-03 16:41:46 -0700215 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700216 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700217 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700218 data = ("test %s %s %s\n" %
219 (self.branch, self.number, self.latest_patchset))
220 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700221 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700222 c = self.addFakeChangeToRepo(msg, files, large)
Clark Boylanb640e052014-04-03 16:41:46 -0700223 ps_files = [{'file': '/COMMIT_MSG',
224 'type': 'ADDED'},
225 {'file': 'README',
226 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700228 ps_files.append({'file': f, 'type': 'ADDED'})
229 d = {'approvals': [],
230 'createdOn': time.time(),
231 'files': ps_files,
232 'number': str(self.latest_patchset),
233 'ref': 'refs/changes/1/%s/%s' % (self.number,
234 self.latest_patchset),
235 'revision': c.hexsha,
236 'uploader': {'email': 'user@example.com',
237 'name': 'User name',
238 'username': 'user'}}
239 self.data['currentPatchSet'] = d
240 self.patchsets.append(d)
241 self.data['submitRecords'] = self.getSubmitRecords()
242
243 def getPatchsetCreatedEvent(self, patchset):
244 event = {"type": "patchset-created",
245 "change": {"project": self.project,
246 "branch": self.branch,
247 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
248 "number": str(self.number),
249 "subject": self.subject,
250 "owner": {"name": "User Name"},
251 "url": "https://hostname/3"},
252 "patchSet": self.patchsets[patchset - 1],
253 "uploader": {"name": "User Name"}}
254 return event
255
256 def getChangeRestoredEvent(self):
257 event = {"type": "change-restored",
258 "change": {"project": self.project,
259 "branch": self.branch,
260 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
261 "number": str(self.number),
262 "subject": self.subject,
263 "owner": {"name": "User Name"},
264 "url": "https://hostname/3"},
265 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100266 "patchSet": self.patchsets[-1],
267 "reason": ""}
268 return event
269
270 def getChangeAbandonedEvent(self):
271 event = {"type": "change-abandoned",
272 "change": {"project": self.project,
273 "branch": self.branch,
274 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
275 "number": str(self.number),
276 "subject": self.subject,
277 "owner": {"name": "User Name"},
278 "url": "https://hostname/3"},
279 "abandoner": {"name": "User Name"},
280 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700281 "reason": ""}
282 return event
283
284 def getChangeCommentEvent(self, patchset):
285 event = {"type": "comment-added",
286 "change": {"project": self.project,
287 "branch": self.branch,
288 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
289 "number": str(self.number),
290 "subject": self.subject,
291 "owner": {"name": "User Name"},
292 "url": "https://hostname/3"},
293 "patchSet": self.patchsets[patchset - 1],
294 "author": {"name": "User Name"},
James E. Blair8b5408c2016-08-08 15:37:46 -0700295 "approvals": [{"type": "code-review",
Clark Boylanb640e052014-04-03 16:41:46 -0700296 "description": "Code-Review",
297 "value": "0"}],
298 "comment": "This is a comment"}
299 return event
300
James E. Blairc2a5ed72017-02-20 14:12:01 -0500301 def getChangeMergedEvent(self):
302 event = {"submitter": {"name": "Jenkins",
303 "username": "jenkins"},
304 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
305 "patchSet": self.patchsets[-1],
306 "change": self.data,
307 "type": "change-merged",
308 "eventCreatedOn": 1487613810}
309 return event
310
James E. Blair8cce42e2016-10-18 08:18:36 -0700311 def getRefUpdatedEvent(self):
312 path = os.path.join(self.upstream_root, self.project)
313 repo = git.Repo(path)
314 oldrev = repo.heads[self.branch].commit.hexsha
315
316 event = {
317 "type": "ref-updated",
318 "submitter": {
319 "name": "User Name",
320 },
321 "refUpdate": {
322 "oldRev": oldrev,
323 "newRev": self.patchsets[-1]['revision'],
324 "refName": self.branch,
325 "project": self.project,
326 }
327 }
328 return event
329
Joshua Hesketh642824b2014-07-01 17:54:59 +1000330 def addApproval(self, category, value, username='reviewer_john',
331 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700332 if not granted_on:
333 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000334 approval = {
335 'description': self.categories[category][0],
336 'type': category,
337 'value': str(value),
338 'by': {
339 'username': username,
340 'email': username + '@example.com',
341 },
342 'grantedOn': int(granted_on)
343 }
Clark Boylanb640e052014-04-03 16:41:46 -0700344 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
345 if x['by']['username'] == username and x['type'] == category:
346 del self.patchsets[-1]['approvals'][i]
347 self.patchsets[-1]['approvals'].append(approval)
348 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000349 'author': {'email': 'author@example.com',
350 'name': 'Patchset Author',
351 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700352 'change': {'branch': self.branch,
353 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
354 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000355 'owner': {'email': 'owner@example.com',
356 'name': 'Change Owner',
357 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700358 'project': self.project,
359 'subject': self.subject,
360 'topic': 'master',
361 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700363 'patchSet': self.patchsets[-1],
364 'type': 'comment-added'}
365 self.data['submitRecords'] = self.getSubmitRecords()
366 return json.loads(json.dumps(event))
367
368 def getSubmitRecords(self):
369 status = {}
370 for cat in self.categories.keys():
371 status[cat] = 0
372
373 for a in self.patchsets[-1]['approvals']:
374 cur = status[a['type']]
375 cat_min, cat_max = self.categories[a['type']][1:]
376 new = int(a['value'])
377 if new == cat_min:
378 cur = new
379 elif abs(new) > abs(cur):
380 cur = new
381 status[a['type']] = cur
382
383 labels = []
384 ok = True
385 for typ, cat in self.categories.items():
386 cur = status[typ]
387 cat_min, cat_max = cat[1:]
388 if cur == cat_min:
389 value = 'REJECT'
390 ok = False
391 elif cur == cat_max:
392 value = 'OK'
393 else:
394 value = 'NEED'
395 ok = False
396 labels.append({'label': cat[0], 'status': value})
397 if ok:
398 return [{'status': 'OK'}]
399 return [{'status': 'NOT_READY',
400 'labels': labels}]
401
402 def setDependsOn(self, other, patchset):
403 self.depends_on_change = other
404 d = {'id': other.data['id'],
405 'number': other.data['number'],
406 'ref': other.patchsets[patchset - 1]['ref']
407 }
408 self.data['dependsOn'] = [d]
409
410 other.needed_by_changes.append(self)
411 needed = other.data.get('neededBy', [])
412 d = {'id': self.data['id'],
413 'number': self.data['number'],
414 'ref': self.patchsets[patchset - 1]['ref'],
415 'revision': self.patchsets[patchset - 1]['revision']
416 }
417 needed.append(d)
418 other.data['neededBy'] = needed
419
420 def query(self):
421 self.queried += 1
422 d = self.data.get('dependsOn')
423 if d:
424 d = d[0]
425 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
426 d['isCurrentPatchSet'] = True
427 else:
428 d['isCurrentPatchSet'] = False
429 return json.loads(json.dumps(self.data))
430
431 def setMerged(self):
432 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000433 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700434 return
435 if self.fail_merge:
436 return
437 self.data['status'] = 'MERGED'
438 self.open = False
439
440 path = os.path.join(self.upstream_root, self.project)
441 repo = git.Repo(path)
442 repo.heads[self.branch].commit = \
443 repo.commit(self.patchsets[-1]['revision'])
444
445 def setReported(self):
446 self.reported += 1
447
448
James E. Blaire511d2f2016-12-08 15:22:26 -0800449class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700450 """A Fake Gerrit connection for use in tests.
451
452 This subclasses
453 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
454 ability for tests to add changes to the fake Gerrit it represents.
455 """
456
Joshua Hesketh352264b2015-08-11 23:42:08 +1000457 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700458
James E. Blaire511d2f2016-12-08 15:22:26 -0800459 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700460 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800461 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000462 connection_config)
463
James E. Blair7fc8daa2016-08-08 15:37:15 -0700464 self.event_queue = Queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700465 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
466 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000467 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700468 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200469 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700470
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700471 def addFakeChange(self, project, branch, subject, status='NEW',
472 files=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700473 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700475 c = FakeGerritChange(self, self.change_number, project, branch,
476 subject, upstream_root=self.upstream_root,
477 status=status, files=files)
Clark Boylanb640e052014-04-03 16:41:46 -0700478 self.changes[self.change_number] = c
479 return c
480
Clark Boylanb640e052014-04-03 16:41:46 -0700481 def review(self, project, changeid, message, action):
482 number, ps = changeid.split(',')
483 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000484
485 # Add the approval back onto the change (ie simulate what gerrit would
486 # do).
487 # Usually when zuul leaves a review it'll create a feedback loop where
488 # zuul's review enters another gerrit event (which is then picked up by
489 # zuul). However, we can't mimic this behaviour (by adding this
490 # approval event into the queue) as it stops jobs from checking what
491 # happens before this event is triggered. If a job needs to see what
492 # happens they can add their own verified event into the queue.
493 # Nevertheless, we can update change with the new review in gerrit.
494
James E. Blair8b5408c2016-08-08 15:37:46 -0700495 for cat in action.keys():
496 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000497 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000498
Clark Boylanb640e052014-04-03 16:41:46 -0700499 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500
Clark Boylanb640e052014-04-03 16:41:46 -0700501 if 'submit' in action:
502 change.setMerged()
503 if message:
504 change.setReported()
505
506 def query(self, number):
507 change = self.changes.get(int(number))
508 if change:
509 return change.query()
510 return {}
511
James E. Blairc494d542014-08-06 09:23:52 -0700512 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700513 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700514 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800515 if query.startswith('change:'):
516 # Query a specific changeid
517 changeid = query[len('change:'):]
518 l = [change.query() for change in self.changes.values()
519 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700520 elif query.startswith('message:'):
521 # Query the content of a commit message
522 msg = query[len('message:'):].strip()
523 l = [change.query() for change in self.changes.values()
524 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800525 else:
526 # Query all open changes
527 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700528 return l
James E. Blairc494d542014-08-06 09:23:52 -0700529
Joshua Hesketh352264b2015-08-11 23:42:08 +1000530 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700531 pass
532
Joshua Hesketh352264b2015-08-11 23:42:08 +1000533 def getGitUrl(self, project):
534 return os.path.join(self.upstream_root, project.name)
535
Clark Boylanb640e052014-04-03 16:41:46 -0700536
Gregory Haynes4fc12542015-04-22 20:38:06 -0700537class GithubChangeReference(git.Reference):
538 _common_path_default = "refs/pull"
539 _points_to_commits_only = True
540
541
542class FakeGithubPullRequest(object):
543
544 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100545 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700546 """Creates a new PR with several commits.
547 Sends an event about opened PR."""
548 self.github = github
549 self.source = github
550 self.number = number
551 self.project = project
552 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100553 self.subject = subject
554 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700555 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100556 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700557 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100558 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100559 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.updated_at = None
561 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100562 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100563 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700564 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100565 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700566 self._updateTimeStamp()
567
Jan Hruban570d01c2016-03-10 21:51:32 +0100568 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100570 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700571 self._updateTimeStamp()
572
Jan Hruban570d01c2016-03-10 21:51:32 +0100573 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
577
578 def getPullRequestOpenedEvent(self):
579 return self._getPullRequestEvent('opened')
580
581 def getPullRequestSynchronizeEvent(self):
582 return self._getPullRequestEvent('synchronize')
583
584 def getPullRequestReopenedEvent(self):
585 return self._getPullRequestEvent('reopened')
586
587 def getPullRequestClosedEvent(self):
588 return self._getPullRequestEvent('closed')
589
590 def addComment(self, message):
591 self.comments.append(message)
592 self._updateTimeStamp()
593
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200594 def getCommentAddedEvent(self, text):
595 name = 'issue_comment'
596 data = {
597 'action': 'created',
598 'issue': {
599 'number': self.number
600 },
601 'comment': {
602 'body': text
603 },
604 'repository': {
605 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100606 },
607 'sender': {
608 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200609 }
610 }
611 return (name, data)
612
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800613 def getReviewAddedEvent(self, review):
614 name = 'pull_request_review'
615 data = {
616 'action': 'submitted',
617 'pull_request': {
618 'number': self.number,
619 'title': self.subject,
620 'updated_at': self.updated_at,
621 'base': {
622 'ref': self.branch,
623 'repo': {
624 'full_name': self.project
625 }
626 },
627 'head': {
628 'sha': self.head_sha
629 }
630 },
631 'review': {
632 'state': review
633 },
634 'repository': {
635 'full_name': self.project
636 },
637 'sender': {
638 'login': 'ghuser'
639 }
640 }
641 return (name, data)
642
Jan Hruban16ad31f2015-11-07 14:39:07 +0100643 def addLabel(self, name):
644 if name not in self.labels:
645 self.labels.append(name)
646 self._updateTimeStamp()
647 return self._getLabelEvent(name)
648
649 def removeLabel(self, name):
650 if name in self.labels:
651 self.labels.remove(name)
652 self._updateTimeStamp()
653 return self._getUnlabelEvent(name)
654
655 def _getLabelEvent(self, label):
656 name = 'pull_request'
657 data = {
658 'action': 'labeled',
659 'pull_request': {
660 'number': self.number,
661 'updated_at': self.updated_at,
662 'base': {
663 'ref': self.branch,
664 'repo': {
665 'full_name': self.project
666 }
667 },
668 'head': {
669 'sha': self.head_sha
670 }
671 },
672 'label': {
673 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100674 },
675 'sender': {
676 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100677 }
678 }
679 return (name, data)
680
681 def _getUnlabelEvent(self, label):
682 name = 'pull_request'
683 data = {
684 'action': 'unlabeled',
685 'pull_request': {
686 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100687 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100688 'updated_at': self.updated_at,
689 'base': {
690 'ref': self.branch,
691 'repo': {
692 'full_name': self.project
693 }
694 },
695 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800696 'sha': self.head_sha,
697 'repo': {
698 'full_name': self.project
699 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100700 }
701 },
702 'label': {
703 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100704 },
705 'sender': {
706 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100707 }
708 }
709 return (name, data)
710
Gregory Haynes4fc12542015-04-22 20:38:06 -0700711 def _getRepo(self):
712 repo_path = os.path.join(self.upstream_root, self.project)
713 return git.Repo(repo_path)
714
715 def _createPRRef(self):
716 repo = self._getRepo()
717 GithubChangeReference.create(
718 repo, self._getPRReference(), 'refs/tags/init')
719
Jan Hruban570d01c2016-03-10 21:51:32 +0100720 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700721 repo = self._getRepo()
722 ref = repo.references[self._getPRReference()]
723 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100724 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100726 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700727 repo.head.reference = ref
728 zuul.merger.merger.reset_repo_to_head(repo)
729 repo.git.clean('-x', '-f', '-d')
730
Jan Hruban570d01c2016-03-10 21:51:32 +0100731 if files:
732 fn = files[0]
733 self.files = files
734 else:
735 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
736 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100737 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700738 fn = os.path.join(repo.working_dir, fn)
739 f = open(fn, 'w')
740 with open(fn, 'w') as f:
741 f.write("test %s %s\n" %
742 (self.branch, self.number))
743 repo.index.add([fn])
744
745 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800746 # Create an empty set of statuses for the given sha,
747 # each sha on a PR may have a status set on it
748 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700749 repo.head.reference = 'master'
750 zuul.merger.merger.reset_repo_to_head(repo)
751 repo.git.clean('-x', '-f', '-d')
752 repo.heads['master'].checkout()
753
754 def _updateTimeStamp(self):
755 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
756
757 def getPRHeadSha(self):
758 repo = self._getRepo()
759 return repo.references[self._getPRReference()].commit.hexsha
760
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800761 def setStatus(self, sha, state, url, description, context, user='zuul'):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800762 # Since we're bypassing github API, which would require a user, we
763 # hard set the user as 'zuul' here.
Jesse Keatingd96e5882017-01-19 13:55:50 -0800764 # insert the status at the top of the list, to simulate that it
765 # is the most recent set status
766 self.statuses[sha].insert(0, ({
Jan Hrubane252a732017-01-03 15:03:09 +0100767 'state': state,
768 'url': url,
Jesse Keatingd96e5882017-01-19 13:55:50 -0800769 'description': description,
770 'context': context,
771 'creator': {
772 'login': user
773 }
774 }))
Jan Hrubane252a732017-01-03 15:03:09 +0100775
Gregory Haynes4fc12542015-04-22 20:38:06 -0700776 def _getPRReference(self):
777 return '%s/head' % self.number
778
779 def _getPullRequestEvent(self, action):
780 name = 'pull_request'
781 data = {
782 'action': action,
783 'number': self.number,
784 'pull_request': {
785 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100786 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700787 'updated_at': self.updated_at,
788 'base': {
789 'ref': self.branch,
790 'repo': {
791 'full_name': self.project
792 }
793 },
794 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800795 'sha': self.head_sha,
796 'repo': {
797 'full_name': self.project
798 }
Gregory Haynes4fc12542015-04-22 20:38:06 -0700799 }
Jan Hruban3b415922016-02-03 13:10:22 +0100800 },
801 'sender': {
802 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700803 }
804 }
805 return (name, data)
806
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800807 def getCommitStatusEvent(self, context, state='success', user='zuul'):
808 name = 'status'
809 data = {
810 'state': state,
811 'sha': self.head_sha,
812 'description': 'Test results for %s: %s' % (self.head_sha, state),
813 'target_url': 'http://zuul/%s' % self.head_sha,
814 'branches': [],
815 'context': context,
816 'sender': {
817 'login': user
818 }
819 }
820 return (name, data)
821
Gregory Haynes4fc12542015-04-22 20:38:06 -0700822
823class FakeGithubConnection(githubconnection.GithubConnection):
824 log = logging.getLogger("zuul.test.FakeGithubConnection")
825
826 def __init__(self, driver, connection_name, connection_config,
827 upstream_root=None):
828 super(FakeGithubConnection, self).__init__(driver, connection_name,
829 connection_config)
830 self.connection_name = connection_name
831 self.pr_number = 0
832 self.pull_requests = []
833 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100834 self.merge_failure = False
835 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700836
Jan Hruban570d01c2016-03-10 21:51:32 +0100837 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 self.pr_number += 1
839 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100840 self, self.pr_number, project, branch, subject, self.upstream_root,
841 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700842 self.pull_requests.append(pull_request)
843 return pull_request
844
Wayne1a78c612015-06-11 17:14:13 -0700845 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
846 if not old_rev:
847 old_rev = '00000000000000000000000000000000'
848 if not new_rev:
849 new_rev = random_sha1()
850 name = 'push'
851 data = {
852 'ref': ref,
853 'before': old_rev,
854 'after': new_rev,
855 'repository': {
856 'full_name': project
857 }
858 }
859 return (name, data)
860
Gregory Haynes4fc12542015-04-22 20:38:06 -0700861 def emitEvent(self, event):
862 """Emulates sending the GitHub webhook event to the connection."""
863 port = self.webapp.server.socket.getsockname()[1]
864 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -0700865 payload = json.dumps(data).encode('utf8')
Gregory Haynes4fc12542015-04-22 20:38:06 -0700866 headers = {'X-Github-Event': name}
867 req = urllib.request.Request(
868 'http://localhost:%s/connection/%s/payload'
869 % (port, self.connection_name),
870 data=payload, headers=headers)
871 urllib.request.urlopen(req)
872
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200873 def getPull(self, project, number):
874 pr = self.pull_requests[number - 1]
875 data = {
876 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100877 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200878 'updated_at': pr.updated_at,
879 'base': {
880 'repo': {
881 'full_name': pr.project
882 },
883 'ref': pr.branch,
884 },
Jan Hruban37615e52015-11-19 14:30:49 +0100885 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200886 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800887 'sha': pr.head_sha,
888 'repo': {
889 'full_name': pr.project
890 }
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200891 }
892 }
893 return data
894
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800895 def getPullBySha(self, sha):
896 prs = list(set([p for p in self.pull_requests if sha == p.head_sha]))
897 if len(prs) > 1:
898 raise Exception('Multiple pulls found with head sha: %s' % sha)
899 pr = prs[0]
900 return self.getPull(pr.project, pr.number)
901
Jan Hruban570d01c2016-03-10 21:51:32 +0100902 def getPullFileNames(self, project, number):
903 pr = self.pull_requests[number - 1]
904 return pr.files
905
Jan Hruban3b415922016-02-03 13:10:22 +0100906 def getUser(self, login):
907 data = {
908 'username': login,
909 'name': 'Github User',
910 'email': 'github.user@example.com'
911 }
912 return data
913
Gregory Haynes4fc12542015-04-22 20:38:06 -0700914 def getGitUrl(self, project):
915 return os.path.join(self.upstream_root, str(project))
916
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200917 def real_getGitUrl(self, project):
918 return super(FakeGithubConnection, self).getGitUrl(project)
919
Gregory Haynes4fc12542015-04-22 20:38:06 -0700920 def getProjectBranches(self, project):
921 """Masks getProjectBranches since we don't have a real github"""
922
923 # just returns master for now
924 return ['master']
925
Jan Hrubane252a732017-01-03 15:03:09 +0100926 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700927 pull_request = self.pull_requests[pr_number - 1]
928 pull_request.addComment(message)
929
Jan Hruban3b415922016-02-03 13:10:22 +0100930 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100931 pull_request = self.pull_requests[pr_number - 1]
932 if self.merge_failure:
933 raise Exception('Pull request was not merged')
934 if self.merge_not_allowed_count > 0:
935 self.merge_not_allowed_count -= 1
936 raise MergeFailure('Merge was not successful due to mergeability'
937 ' conflict')
938 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100939 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100940
Jesse Keatingd96e5882017-01-19 13:55:50 -0800941 def getCommitStatuses(self, project, sha):
942 owner, proj = project.split('/')
943 for pr in self.pull_requests:
944 pr_owner, pr_project = pr.project.split('/')
945 if (pr_owner == owner and pr_project == proj and
946 pr.head_sha == sha):
947 return pr.statuses[sha]
948
Jan Hrubane252a732017-01-03 15:03:09 +0100949 def setCommitStatus(self, project, sha, state,
950 url='', description='', context=''):
951 owner, proj = project.split('/')
952 for pr in self.pull_requests:
953 pr_owner, pr_project = pr.project.split('/')
954 if (pr_owner == owner and pr_project == proj and
955 pr.head_sha == sha):
Jesse Keatingd96e5882017-01-19 13:55:50 -0800956 pr.setStatus(sha, state, url, description, context)
Jan Hrubane252a732017-01-03 15:03:09 +0100957
Jan Hruban16ad31f2015-11-07 14:39:07 +0100958 def labelPull(self, project, pr_number, label):
959 pull_request = self.pull_requests[pr_number - 1]
960 pull_request.addLabel(label)
961
962 def unlabelPull(self, project, pr_number, label):
963 pull_request = self.pull_requests[pr_number - 1]
964 pull_request.removeLabel(label)
965
Gregory Haynes4fc12542015-04-22 20:38:06 -0700966
Clark Boylanb640e052014-04-03 16:41:46 -0700967class BuildHistory(object):
968 def __init__(self, **kw):
969 self.__dict__.update(kw)
970
971 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700972 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
973 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700974
975
976class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200977 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700978 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700979 self.url = url
980
981 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700982 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700983 path = res.path
984 project = '/'.join(path.split('/')[2:-2])
985 ret = '001e# service=git-upload-pack\n'
986 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
987 'multi_ack thin-pack side-band side-band-64k ofs-delta '
988 'shallow no-progress include-tag multi_ack_detailed no-done\n')
989 path = os.path.join(self.upstream_root, project)
990 repo = git.Repo(path)
991 for ref in repo.refs:
992 r = ref.object.hexsha + ' ' + ref.path + '\n'
993 ret += '%04x%s' % (len(r) + 4, r)
994 ret += '0000'
995 return ret
996
997
Clark Boylanb640e052014-04-03 16:41:46 -0700998class FakeStatsd(threading.Thread):
999 def __init__(self):
1000 threading.Thread.__init__(self)
1001 self.daemon = True
1002 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
1003 self.sock.bind(('', 0))
1004 self.port = self.sock.getsockname()[1]
1005 self.wake_read, self.wake_write = os.pipe()
1006 self.stats = []
1007
1008 def run(self):
1009 while True:
1010 poll = select.poll()
1011 poll.register(self.sock, select.POLLIN)
1012 poll.register(self.wake_read, select.POLLIN)
1013 ret = poll.poll()
1014 for (fd, event) in ret:
1015 if fd == self.sock.fileno():
1016 data = self.sock.recvfrom(1024)
1017 if not data:
1018 return
1019 self.stats.append(data[0])
1020 if fd == self.wake_read:
1021 return
1022
1023 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001024 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001025
1026
James E. Blaire1767bc2016-08-02 10:00:27 -07001027class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001028 log = logging.getLogger("zuul.test")
1029
Paul Belanger174a8272017-03-14 13:20:10 -04001030 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001031 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001032 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001033 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001034 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001035 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001036 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -07001037 # TODOv3(jeblair): self.node is really "the image of the node
1038 # assigned". We should rename it (self.node_image?) if we
1039 # keep using it like this, or we may end up exposing more of
1040 # the complexity around multi-node jobs here
1041 # (self.nodes[0].image?)
1042 self.node = None
1043 if len(self.parameters.get('nodes')) == 1:
1044 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001045 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001046 self.pipeline = self.parameters['ZUUL_PIPELINE']
1047 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001048 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001049 self.wait_condition = threading.Condition()
1050 self.waiting = False
1051 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001052 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001053 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001054 self.changes = None
1055 if 'ZUUL_CHANGE_IDS' in self.parameters:
1056 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001057
James E. Blair3158e282016-08-19 09:34:11 -07001058 def __repr__(self):
1059 waiting = ''
1060 if self.waiting:
1061 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001062 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1063 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001064
Clark Boylanb640e052014-04-03 16:41:46 -07001065 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001066 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001067 self.wait_condition.acquire()
1068 self.wait_condition.notify()
1069 self.waiting = False
1070 self.log.debug("Build %s released" % self.unique)
1071 self.wait_condition.release()
1072
1073 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001074 """Return whether this build is being held.
1075
1076 :returns: Whether the build is being held.
1077 :rtype: bool
1078 """
1079
Clark Boylanb640e052014-04-03 16:41:46 -07001080 self.wait_condition.acquire()
1081 if self.waiting:
1082 ret = True
1083 else:
1084 ret = False
1085 self.wait_condition.release()
1086 return ret
1087
1088 def _wait(self):
1089 self.wait_condition.acquire()
1090 self.waiting = True
1091 self.log.debug("Build %s waiting" % self.unique)
1092 self.wait_condition.wait()
1093 self.wait_condition.release()
1094
1095 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001096 self.log.debug('Running build %s' % self.unique)
1097
Paul Belanger174a8272017-03-14 13:20:10 -04001098 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001099 self.log.debug('Holding build %s' % self.unique)
1100 self._wait()
1101 self.log.debug("Build %s continuing" % self.unique)
1102
James E. Blair412fba82017-01-26 15:00:50 -08001103 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001104 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001105 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001106 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001107 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001108 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001109 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001110
James E. Blaire1767bc2016-08-02 10:00:27 -07001111 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001112
James E. Blaira5dba232016-08-08 15:53:24 -07001113 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001114 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001115 for change in changes:
1116 if self.hasChanges(change):
1117 return True
1118 return False
1119
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 def hasChanges(self, *changes):
1121 """Return whether this build has certain changes in its git repos.
1122
1123 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001124 are expected to be present (in order) in the git repository of
1125 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001126
1127 :returns: Whether the build has the indicated changes.
1128 :rtype: bool
1129
1130 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001131 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001132 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001133 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001134 try:
1135 repo = git.Repo(path)
1136 except NoSuchPathError as e:
1137 self.log.debug('%s' % e)
1138 return False
1139 ref = self.parameters['ZUUL_REF']
1140 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1141 commit_message = '%s-1' % change.subject
1142 self.log.debug("Checking if build %s has changes; commit_message "
1143 "%s; repo_messages %s" % (self, commit_message,
1144 repo_messages))
1145 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001146 self.log.debug(" messages do not match")
1147 return False
1148 self.log.debug(" OK")
1149 return True
1150
Clark Boylanb640e052014-04-03 16:41:46 -07001151
Paul Belanger174a8272017-03-14 13:20:10 -04001152class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1153 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001154
Paul Belanger174a8272017-03-14 13:20:10 -04001155 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001156 they will report that they have started but then pause until
1157 released before reporting completion. This attribute may be
1158 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001159 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001160 be explicitly released.
1161
1162 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001163 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001164 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001165 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001166 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001167 self.hold_jobs_in_build = False
1168 self.lock = threading.Lock()
1169 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001170 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001171 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001172 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001173
James E. Blaira5dba232016-08-08 15:53:24 -07001174 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001175 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001176
1177 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001178 :arg Change change: The :py:class:`~tests.base.FakeChange`
1179 instance which should cause the job to fail. This job
1180 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001181
1182 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001183 l = self.fail_tests.get(name, [])
1184 l.append(change)
1185 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001186
James E. Blair962220f2016-08-03 11:22:38 -07001187 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001188 """Release a held build.
1189
1190 :arg str regex: A regular expression which, if supplied, will
1191 cause only builds with matching names to be released. If
1192 not supplied, all builds will be released.
1193
1194 """
James E. Blair962220f2016-08-03 11:22:38 -07001195 builds = self.running_builds[:]
1196 self.log.debug("Releasing build %s (%s)" % (regex,
1197 len(self.running_builds)))
1198 for build in builds:
1199 if not regex or re.match(regex, build.name):
1200 self.log.debug("Releasing build %s" %
1201 (build.parameters['ZUUL_UUID']))
1202 build.release()
1203 else:
1204 self.log.debug("Not releasing build %s" %
1205 (build.parameters['ZUUL_UUID']))
1206 self.log.debug("Done releasing builds %s (%s)" %
1207 (regex, len(self.running_builds)))
1208
Paul Belanger174a8272017-03-14 13:20:10 -04001209 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001210 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001211 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001212 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001213 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001214 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001215 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001216 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001217 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1218 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001219
1220 def stopJob(self, job):
1221 self.log.debug("handle stop")
1222 parameters = json.loads(job.arguments)
1223 uuid = parameters['uuid']
1224 for build in self.running_builds:
1225 if build.unique == uuid:
1226 build.aborted = True
1227 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001228 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001229
James E. Blaira002b032017-04-18 10:35:48 -07001230 def stop(self):
1231 for build in self.running_builds:
1232 build.release()
1233 super(RecordingExecutorServer, self).stop()
1234
Joshua Hesketh50c21782016-10-13 21:34:14 +11001235
Paul Belanger174a8272017-03-14 13:20:10 -04001236class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001237 def doMergeChanges(self, items):
1238 # Get a merger in order to update the repos involved in this job.
1239 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1240 if not commit: # merge conflict
1241 self.recordResult('MERGER_FAILURE')
1242 return commit
1243
1244 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001245 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001246 self.executor_server.lock.acquire()
1247 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001248 BuildHistory(name=build.name, result=result, changes=build.changes,
1249 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001250 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001251 pipeline=build.parameters['ZUUL_PIPELINE'])
1252 )
Paul Belanger174a8272017-03-14 13:20:10 -04001253 self.executor_server.running_builds.remove(build)
1254 del self.executor_server.job_builds[self.job.unique]
1255 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001256
1257 def runPlaybooks(self, args):
1258 build = self.executor_server.job_builds[self.job.unique]
1259 build.jobdir = self.jobdir
1260
1261 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1262 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001263 return result
1264
Monty Taylore6562aa2017-02-20 07:37:39 -05001265 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001266 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001267
Paul Belanger174a8272017-03-14 13:20:10 -04001268 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001269 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001270 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001271 else:
1272 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001273 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001274
James E. Blairad8dca02017-02-21 11:48:32 -05001275 def getHostList(self, args):
1276 self.log.debug("hostlist")
1277 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001278 for host in hosts:
1279 host['host_vars']['ansible_connection'] = 'local'
1280
1281 hosts.append(dict(
1282 name='localhost',
1283 host_vars=dict(ansible_connection='local'),
1284 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001285 return hosts
1286
James E. Blairf5dbd002015-12-23 15:26:17 -08001287
Clark Boylanb640e052014-04-03 16:41:46 -07001288class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001289 """A Gearman server for use in tests.
1290
1291 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1292 added to the queue but will not be distributed to workers
1293 until released. This attribute may be changed at any time and
1294 will take effect for subsequently enqueued jobs, but
1295 previously held jobs will still need to be explicitly
1296 released.
1297
1298 """
1299
Clark Boylanb640e052014-04-03 16:41:46 -07001300 def __init__(self):
1301 self.hold_jobs_in_queue = False
1302 super(FakeGearmanServer, self).__init__(0)
1303
1304 def getJobForConnection(self, connection, peek=False):
1305 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1306 for job in queue:
1307 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001308 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001309 job.waiting = self.hold_jobs_in_queue
1310 else:
1311 job.waiting = False
1312 if job.waiting:
1313 continue
1314 if job.name in connection.functions:
1315 if not peek:
1316 queue.remove(job)
1317 connection.related_jobs[job.handle] = job
1318 job.worker_connection = connection
1319 job.running = True
1320 return job
1321 return None
1322
1323 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001324 """Release a held job.
1325
1326 :arg str regex: A regular expression which, if supplied, will
1327 cause only jobs with matching names to be released. If
1328 not supplied, all jobs will be released.
1329 """
Clark Boylanb640e052014-04-03 16:41:46 -07001330 released = False
1331 qlen = (len(self.high_queue) + len(self.normal_queue) +
1332 len(self.low_queue))
1333 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1334 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001335 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001336 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001337 parameters = json.loads(job.arguments)
1338 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001339 self.log.debug("releasing queued job %s" %
1340 job.unique)
1341 job.waiting = False
1342 released = True
1343 else:
1344 self.log.debug("not releasing queued job %s" %
1345 job.unique)
1346 if released:
1347 self.wakeConnections()
1348 qlen = (len(self.high_queue) + len(self.normal_queue) +
1349 len(self.low_queue))
1350 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1351
1352
1353class FakeSMTP(object):
1354 log = logging.getLogger('zuul.FakeSMTP')
1355
1356 def __init__(self, messages, server, port):
1357 self.server = server
1358 self.port = port
1359 self.messages = messages
1360
1361 def sendmail(self, from_email, to_email, msg):
1362 self.log.info("Sending email from %s, to %s, with msg %s" % (
1363 from_email, to_email, msg))
1364
1365 headers = msg.split('\n\n', 1)[0]
1366 body = msg.split('\n\n', 1)[1]
1367
1368 self.messages.append(dict(
1369 from_email=from_email,
1370 to_email=to_email,
1371 msg=msg,
1372 headers=headers,
1373 body=body,
1374 ))
1375
1376 return True
1377
1378 def quit(self):
1379 return True
1380
1381
James E. Blairdce6cea2016-12-20 16:45:32 -08001382class FakeNodepool(object):
1383 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001384 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001385
1386 log = logging.getLogger("zuul.test.FakeNodepool")
1387
1388 def __init__(self, host, port, chroot):
1389 self.client = kazoo.client.KazooClient(
1390 hosts='%s:%s%s' % (host, port, chroot))
1391 self.client.start()
1392 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001393 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001394 self.thread = threading.Thread(target=self.run)
1395 self.thread.daemon = True
1396 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001397 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001398
1399 def stop(self):
1400 self._running = False
1401 self.thread.join()
1402 self.client.stop()
1403 self.client.close()
1404
1405 def run(self):
1406 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001407 try:
1408 self._run()
1409 except Exception:
1410 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001411 time.sleep(0.1)
1412
1413 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001414 if self.paused:
1415 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001416 for req in self.getNodeRequests():
1417 self.fulfillRequest(req)
1418
1419 def getNodeRequests(self):
1420 try:
1421 reqids = self.client.get_children(self.REQUEST_ROOT)
1422 except kazoo.exceptions.NoNodeError:
1423 return []
1424 reqs = []
1425 for oid in sorted(reqids):
1426 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001427 try:
1428 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001429 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001430 data['_oid'] = oid
1431 reqs.append(data)
1432 except kazoo.exceptions.NoNodeError:
1433 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001434 return reqs
1435
James E. Blaire18d4602017-01-05 11:17:28 -08001436 def getNodes(self):
1437 try:
1438 nodeids = self.client.get_children(self.NODE_ROOT)
1439 except kazoo.exceptions.NoNodeError:
1440 return []
1441 nodes = []
1442 for oid in sorted(nodeids):
1443 path = self.NODE_ROOT + '/' + oid
1444 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001445 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001446 data['_oid'] = oid
1447 try:
1448 lockfiles = self.client.get_children(path + '/lock')
1449 except kazoo.exceptions.NoNodeError:
1450 lockfiles = []
1451 if lockfiles:
1452 data['_lock'] = True
1453 else:
1454 data['_lock'] = False
1455 nodes.append(data)
1456 return nodes
1457
James E. Blaira38c28e2017-01-04 10:33:20 -08001458 def makeNode(self, request_id, node_type):
1459 now = time.time()
1460 path = '/nodepool/nodes/'
1461 data = dict(type=node_type,
1462 provider='test-provider',
1463 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001464 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001465 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001466 public_ipv4='127.0.0.1',
1467 private_ipv4=None,
1468 public_ipv6=None,
1469 allocated_to=request_id,
1470 state='ready',
1471 state_time=now,
1472 created_time=now,
1473 updated_time=now,
1474 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001475 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001476 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001477 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001478 path = self.client.create(path, data,
1479 makepath=True,
1480 sequence=True)
1481 nodeid = path.split("/")[-1]
1482 return nodeid
1483
James E. Blair6ab79e02017-01-06 10:10:17 -08001484 def addFailRequest(self, request):
1485 self.fail_requests.add(request['_oid'])
1486
James E. Blairdce6cea2016-12-20 16:45:32 -08001487 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001488 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001489 return
1490 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001491 oid = request['_oid']
1492 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001493
James E. Blair6ab79e02017-01-06 10:10:17 -08001494 if oid in self.fail_requests:
1495 request['state'] = 'failed'
1496 else:
1497 request['state'] = 'fulfilled'
1498 nodes = []
1499 for node in request['node_types']:
1500 nodeid = self.makeNode(oid, node)
1501 nodes.append(nodeid)
1502 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001503
James E. Blaira38c28e2017-01-04 10:33:20 -08001504 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001505 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001506 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001507 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001508 try:
1509 self.client.set(path, data)
1510 except kazoo.exceptions.NoNodeError:
1511 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001512
1513
James E. Blair498059b2016-12-20 13:50:13 -08001514class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001515 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001516 super(ChrootedKazooFixture, self).__init__()
1517
1518 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1519 if ':' in zk_host:
1520 host, port = zk_host.split(':')
1521 else:
1522 host = zk_host
1523 port = None
1524
1525 self.zookeeper_host = host
1526
1527 if not port:
1528 self.zookeeper_port = 2181
1529 else:
1530 self.zookeeper_port = int(port)
1531
Clark Boylan621ec9a2017-04-07 17:41:33 -07001532 self.test_id = test_id
1533
James E. Blair498059b2016-12-20 13:50:13 -08001534 def _setUp(self):
1535 # Make sure the test chroot paths do not conflict
1536 random_bits = ''.join(random.choice(string.ascii_lowercase +
1537 string.ascii_uppercase)
1538 for x in range(8))
1539
Clark Boylan621ec9a2017-04-07 17:41:33 -07001540 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001541 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1542
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001543 self.addCleanup(self._cleanup)
1544
James E. Blair498059b2016-12-20 13:50:13 -08001545 # Ensure the chroot path exists and clean up any pre-existing znodes.
1546 _tmp_client = kazoo.client.KazooClient(
1547 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1548 _tmp_client.start()
1549
1550 if _tmp_client.exists(self.zookeeper_chroot):
1551 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1552
1553 _tmp_client.ensure_path(self.zookeeper_chroot)
1554 _tmp_client.stop()
1555 _tmp_client.close()
1556
James E. Blair498059b2016-12-20 13:50:13 -08001557 def _cleanup(self):
1558 '''Remove the chroot path.'''
1559 # Need a non-chroot'ed client to remove the chroot path
1560 _tmp_client = kazoo.client.KazooClient(
1561 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1562 _tmp_client.start()
1563 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1564 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001565 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001566
1567
Joshua Heskethd78b4482015-09-14 16:56:34 -06001568class MySQLSchemaFixture(fixtures.Fixture):
1569 def setUp(self):
1570 super(MySQLSchemaFixture, self).setUp()
1571
1572 random_bits = ''.join(random.choice(string.ascii_lowercase +
1573 string.ascii_uppercase)
1574 for x in range(8))
1575 self.name = '%s_%s' % (random_bits, os.getpid())
1576 self.passwd = uuid.uuid4().hex
1577 db = pymysql.connect(host="localhost",
1578 user="openstack_citest",
1579 passwd="openstack_citest",
1580 db="openstack_citest")
1581 cur = db.cursor()
1582 cur.execute("create database %s" % self.name)
1583 cur.execute(
1584 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1585 (self.name, self.name, self.passwd))
1586 cur.execute("flush privileges")
1587
1588 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1589 self.passwd,
1590 self.name)
1591 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1592 self.addCleanup(self.cleanup)
1593
1594 def cleanup(self):
1595 db = pymysql.connect(host="localhost",
1596 user="openstack_citest",
1597 passwd="openstack_citest",
1598 db="openstack_citest")
1599 cur = db.cursor()
1600 cur.execute("drop database %s" % self.name)
1601 cur.execute("drop user '%s'@'localhost'" % self.name)
1602 cur.execute("flush privileges")
1603
1604
Maru Newby3fe5f852015-01-13 04:22:14 +00001605class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001606 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001607 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001608
James E. Blair1c236df2017-02-01 14:07:24 -08001609 def attachLogs(self, *args):
1610 def reader():
1611 self._log_stream.seek(0)
1612 while True:
1613 x = self._log_stream.read(4096)
1614 if not x:
1615 break
1616 yield x.encode('utf8')
1617 content = testtools.content.content_from_reader(
1618 reader,
1619 testtools.content_type.UTF8_TEXT,
1620 False)
1621 self.addDetail('logging', content)
1622
Clark Boylanb640e052014-04-03 16:41:46 -07001623 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001624 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001625 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1626 try:
1627 test_timeout = int(test_timeout)
1628 except ValueError:
1629 # If timeout value is invalid do not set a timeout.
1630 test_timeout = 0
1631 if test_timeout > 0:
1632 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1633
1634 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1635 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1636 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1637 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1638 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1639 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1640 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1641 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1642 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1643 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001644 self._log_stream = StringIO()
1645 self.addOnException(self.attachLogs)
1646 else:
1647 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001648
James E. Blair1c236df2017-02-01 14:07:24 -08001649 handler = logging.StreamHandler(self._log_stream)
1650 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1651 '%(levelname)-8s %(message)s')
1652 handler.setFormatter(formatter)
1653
1654 logger = logging.getLogger()
1655 logger.setLevel(logging.DEBUG)
1656 logger.addHandler(handler)
1657
Clark Boylan3410d532017-04-25 12:35:29 -07001658 # Make sure we don't carry old handlers around in process state
1659 # which slows down test runs
1660 self.addCleanup(logger.removeHandler, handler)
1661 self.addCleanup(handler.close)
1662 self.addCleanup(handler.flush)
1663
James E. Blair1c236df2017-02-01 14:07:24 -08001664 # NOTE(notmorgan): Extract logging overrides for specific
1665 # libraries from the OS_LOG_DEFAULTS env and create loggers
1666 # for each. This is used to limit the output during test runs
1667 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001668 log_defaults_from_env = os.environ.get(
1669 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001670 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001671
James E. Blairdce6cea2016-12-20 16:45:32 -08001672 if log_defaults_from_env:
1673 for default in log_defaults_from_env.split(','):
1674 try:
1675 name, level_str = default.split('=', 1)
1676 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001677 logger = logging.getLogger(name)
1678 logger.setLevel(level)
1679 logger.addHandler(handler)
1680 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001681 except ValueError:
1682 # NOTE(notmorgan): Invalid format of the log default,
1683 # skip and don't try and apply a logger for the
1684 # specified module
1685 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001686
Maru Newby3fe5f852015-01-13 04:22:14 +00001687
1688class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001689 """A test case with a functioning Zuul.
1690
1691 The following class variables are used during test setup and can
1692 be overidden by subclasses but are effectively read-only once a
1693 test method starts running:
1694
1695 :cvar str config_file: This points to the main zuul config file
1696 within the fixtures directory. Subclasses may override this
1697 to obtain a different behavior.
1698
1699 :cvar str tenant_config_file: This is the tenant config file
1700 (which specifies from what git repos the configuration should
1701 be loaded). It defaults to the value specified in
1702 `config_file` but can be overidden by subclasses to obtain a
1703 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001704 configuration. See also the :py:func:`simple_layout`
1705 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001706
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001707 :cvar bool create_project_keys: Indicates whether Zuul should
1708 auto-generate keys for each project, or whether the test
1709 infrastructure should insert dummy keys to save time during
1710 startup. Defaults to False.
1711
James E. Blaire7b99a02016-08-05 14:27:34 -07001712 The following are instance variables that are useful within test
1713 methods:
1714
1715 :ivar FakeGerritConnection fake_<connection>:
1716 A :py:class:`~tests.base.FakeGerritConnection` will be
1717 instantiated for each connection present in the config file
1718 and stored here. For instance, `fake_gerrit` will hold the
1719 FakeGerritConnection object for a connection named `gerrit`.
1720
1721 :ivar FakeGearmanServer gearman_server: An instance of
1722 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1723 server that all of the Zuul components in this test use to
1724 communicate with each other.
1725
Paul Belanger174a8272017-03-14 13:20:10 -04001726 :ivar RecordingExecutorServer executor_server: An instance of
1727 :py:class:`~tests.base.RecordingExecutorServer` which is the
1728 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001729
1730 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1731 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001732 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001733 list upon completion.
1734
1735 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1736 objects representing completed builds. They are appended to
1737 the list in the order they complete.
1738
1739 """
1740
James E. Blair83005782015-12-11 14:46:03 -08001741 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001742 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001743 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001744
1745 def _startMerger(self):
1746 self.merge_server = zuul.merger.server.MergeServer(self.config,
1747 self.connections)
1748 self.merge_server.start()
1749
Maru Newby3fe5f852015-01-13 04:22:14 +00001750 def setUp(self):
1751 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001752
1753 self.setupZK()
1754
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001755 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001756 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001757 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1758 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001759 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001760 tmp_root = tempfile.mkdtemp(
1761 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001762 self.test_root = os.path.join(tmp_root, "zuul-test")
1763 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001764 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001765 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001766 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001767
1768 if os.path.exists(self.test_root):
1769 shutil.rmtree(self.test_root)
1770 os.makedirs(self.test_root)
1771 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001772 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001773
1774 # Make per test copy of Configuration.
1775 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001776 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001777 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001778 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001779 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001780 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001781 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001782
Clark Boylanb640e052014-04-03 16:41:46 -07001783 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001784 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1785 # see: https://github.com/jsocol/pystatsd/issues/61
1786 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001787 os.environ['STATSD_PORT'] = str(self.statsd.port)
1788 self.statsd.start()
1789 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001790 reload_module(statsd)
1791 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001792
1793 self.gearman_server = FakeGearmanServer()
1794
1795 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001796 self.log.info("Gearman server on port %s" %
1797 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001798
James E. Blaire511d2f2016-12-08 15:22:26 -08001799 gerritsource.GerritSource.replication_timeout = 1.5
1800 gerritsource.GerritSource.replication_retry_interval = 0.5
1801 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001802
Joshua Hesketh352264b2015-08-11 23:42:08 +10001803 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001804
Jan Hruban7083edd2015-08-21 14:00:54 +02001805 self.webapp = zuul.webapp.WebApp(
1806 self.sched, port=0, listen_address='127.0.0.1')
1807
Jan Hruban6b71aff2015-10-22 16:58:08 +02001808 self.event_queues = [
1809 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001810 self.sched.trigger_event_queue,
1811 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001812 ]
1813
James E. Blairfef78942016-03-11 16:28:56 -08001814 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001815 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001816
Clark Boylanb640e052014-04-03 16:41:46 -07001817 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001818 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001819 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001820 return FakeURLOpener(self.upstream_root, *args, **kw)
1821
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001822 old_urlopen = urllib.request.urlopen
1823 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001824
James E. Blair3f876d52016-07-22 13:07:14 -07001825 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001826
Paul Belanger174a8272017-03-14 13:20:10 -04001827 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001828 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001829 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001830 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001831 _test_root=self.test_root,
1832 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001833 self.executor_server.start()
1834 self.history = self.executor_server.build_history
1835 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001836
Paul Belanger174a8272017-03-14 13:20:10 -04001837 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001838 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001839 self.merge_client = zuul.merger.client.MergeClient(
1840 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001841 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001842 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001843 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001844
James E. Blair0d5a36e2017-02-21 10:53:44 -05001845 self.fake_nodepool = FakeNodepool(
1846 self.zk_chroot_fixture.zookeeper_host,
1847 self.zk_chroot_fixture.zookeeper_port,
1848 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001849
Paul Belanger174a8272017-03-14 13:20:10 -04001850 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001851 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001852 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001853 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001854
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001855 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001856
1857 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001858 self.webapp.start()
1859 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001860 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001861 # Cleanups are run in reverse order
1862 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001863 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001864 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001865
James E. Blairb9c0d772017-03-03 14:34:49 -08001866 self.sched.reconfigure(self.config)
1867 self.sched.resume()
1868
James E. Blairfef78942016-03-11 16:28:56 -08001869 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001870 # Set up gerrit related fakes
1871 # Set a changes database so multiple FakeGerrit's can report back to
1872 # a virtual canonical database given by the configured hostname
1873 self.gerrit_changes_dbs = {}
1874
1875 def getGerritConnection(driver, name, config):
1876 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1877 con = FakeGerritConnection(driver, name, config,
1878 changes_db=db,
1879 upstream_root=self.upstream_root)
1880 self.event_queues.append(con.event_queue)
1881 setattr(self, 'fake_' + name, con)
1882 return con
1883
1884 self.useFixture(fixtures.MonkeyPatch(
1885 'zuul.driver.gerrit.GerritDriver.getConnection',
1886 getGerritConnection))
1887
Gregory Haynes4fc12542015-04-22 20:38:06 -07001888 def getGithubConnection(driver, name, config):
1889 con = FakeGithubConnection(driver, name, config,
1890 upstream_root=self.upstream_root)
1891 setattr(self, 'fake_' + name, con)
1892 return con
1893
1894 self.useFixture(fixtures.MonkeyPatch(
1895 'zuul.driver.github.GithubDriver.getConnection',
1896 getGithubConnection))
1897
James E. Blaire511d2f2016-12-08 15:22:26 -08001898 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001899 # TODO(jhesketh): This should come from lib.connections for better
1900 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001901 # Register connections from the config
1902 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001903
Joshua Hesketh352264b2015-08-11 23:42:08 +10001904 def FakeSMTPFactory(*args, **kw):
1905 args = [self.smtp_messages] + list(args)
1906 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001907
Joshua Hesketh352264b2015-08-11 23:42:08 +10001908 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001909
James E. Blaire511d2f2016-12-08 15:22:26 -08001910 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001911 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001912 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001913
James E. Blair83005782015-12-11 14:46:03 -08001914 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001915 # This creates the per-test configuration object. It can be
1916 # overriden by subclasses, but should not need to be since it
1917 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001918 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001919 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001920
1921 if not self.setupSimpleLayout():
1922 if hasattr(self, 'tenant_config_file'):
1923 self.config.set('zuul', 'tenant_config',
1924 self.tenant_config_file)
1925 git_path = os.path.join(
1926 os.path.dirname(
1927 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1928 'git')
1929 if os.path.exists(git_path):
1930 for reponame in os.listdir(git_path):
1931 project = reponame.replace('_', '/')
1932 self.copyDirToRepo(project,
1933 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001934 self.setupAllProjectKeys()
1935
James E. Blair06cc3922017-04-19 10:08:10 -07001936 def setupSimpleLayout(self):
1937 # If the test method has been decorated with a simple_layout,
1938 # use that instead of the class tenant_config_file. Set up a
1939 # single config-project with the specified layout, and
1940 # initialize repos for all of the 'project' entries which
1941 # appear in the layout.
1942 test_name = self.id().split('.')[-1]
1943 test = getattr(self, test_name)
1944 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001945 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001946 else:
1947 return False
1948
James E. Blairb70e55a2017-04-19 12:57:02 -07001949 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001950 path = os.path.join(FIXTURE_DIR, path)
1951 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001952 data = f.read()
1953 layout = yaml.safe_load(data)
1954 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001955 untrusted_projects = []
1956 for item in layout:
1957 if 'project' in item:
1958 name = item['project']['name']
1959 untrusted_projects.append(name)
1960 self.init_repo(name)
1961 self.addCommitToRepo(name, 'initial commit',
1962 files={'README': ''},
1963 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001964 if 'job' in item:
1965 jobname = item['job']['name']
1966 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001967
1968 root = os.path.join(self.test_root, "config")
1969 if not os.path.exists(root):
1970 os.makedirs(root)
1971 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1972 config = [{'tenant':
1973 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001974 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001975 {'config-projects': ['common-config'],
1976 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07001977 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07001978 f.close()
1979 self.config.set('zuul', 'tenant_config',
1980 os.path.join(FIXTURE_DIR, f.name))
1981
1982 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001983 self.addCommitToRepo('common-config', 'add content from fixture',
1984 files, branch='master', tag='init')
1985
1986 return True
1987
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001988 def setupAllProjectKeys(self):
1989 if self.create_project_keys:
1990 return
1991
1992 path = self.config.get('zuul', 'tenant_config')
1993 with open(os.path.join(FIXTURE_DIR, path)) as f:
1994 tenant_config = yaml.safe_load(f.read())
1995 for tenant in tenant_config:
1996 sources = tenant['tenant']['source']
1997 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001998 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001999 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002000 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002001 self.setupProjectKeys(source, project)
2002
2003 def setupProjectKeys(self, source, project):
2004 # Make sure we set up an RSA key for the project so that we
2005 # don't spend time generating one:
2006
2007 key_root = os.path.join(self.state_root, 'keys')
2008 if not os.path.isdir(key_root):
2009 os.mkdir(key_root, 0o700)
2010 private_key_file = os.path.join(key_root, source, project + '.pem')
2011 private_key_dir = os.path.dirname(private_key_file)
2012 self.log.debug("Installing test keys for project %s at %s" % (
2013 project, private_key_file))
2014 if not os.path.isdir(private_key_dir):
2015 os.makedirs(private_key_dir)
2016 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2017 with open(private_key_file, 'w') as o:
2018 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002019
James E. Blair498059b2016-12-20 13:50:13 -08002020 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002021 self.zk_chroot_fixture = self.useFixture(
2022 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002023 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002024 self.zk_chroot_fixture.zookeeper_host,
2025 self.zk_chroot_fixture.zookeeper_port,
2026 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002027
James E. Blair96c6bf82016-01-15 16:20:40 -08002028 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002029 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002030
2031 files = {}
2032 for (dirpath, dirnames, filenames) in os.walk(source_path):
2033 for filename in filenames:
2034 test_tree_filepath = os.path.join(dirpath, filename)
2035 common_path = os.path.commonprefix([test_tree_filepath,
2036 source_path])
2037 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2038 with open(test_tree_filepath, 'r') as f:
2039 content = f.read()
2040 files[relative_filepath] = content
2041 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002042 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002043
James E. Blaire18d4602017-01-05 11:17:28 -08002044 def assertNodepoolState(self):
2045 # Make sure that there are no pending requests
2046
2047 requests = self.fake_nodepool.getNodeRequests()
2048 self.assertEqual(len(requests), 0)
2049
2050 nodes = self.fake_nodepool.getNodes()
2051 for node in nodes:
2052 self.assertFalse(node['_lock'], "Node %s is locked" %
2053 (node['_oid'],))
2054
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002055 def assertNoGeneratedKeys(self):
2056 # Make sure that Zuul did not generate any project keys
2057 # (unless it was supposed to).
2058
2059 if self.create_project_keys:
2060 return
2061
2062 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2063 test_key = i.read()
2064
2065 key_root = os.path.join(self.state_root, 'keys')
2066 for root, dirname, files in os.walk(key_root):
2067 for fn in files:
2068 with open(os.path.join(root, fn)) as f:
2069 self.assertEqual(test_key, f.read())
2070
Clark Boylanb640e052014-04-03 16:41:46 -07002071 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002072 self.log.debug("Assert final state")
2073 # Make sure no jobs are running
2074 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002075 # Make sure that git.Repo objects have been garbage collected.
2076 repos = []
2077 gc.collect()
2078 for obj in gc.get_objects():
2079 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002080 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002081 repos.append(obj)
2082 self.assertEqual(len(repos), 0)
2083 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002084 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002085 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002086 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002087 for tenant in self.sched.abide.tenants.values():
2088 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002089 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002090 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002091
2092 def shutdown(self):
2093 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002094 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002095 self.merge_server.stop()
2096 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002097 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002098 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002099 self.sched.stop()
2100 self.sched.join()
2101 self.statsd.stop()
2102 self.statsd.join()
2103 self.webapp.stop()
2104 self.webapp.join()
2105 self.rpc.stop()
2106 self.rpc.join()
2107 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002108 self.fake_nodepool.stop()
2109 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002110 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002111 # we whitelist watchdog threads as they have relatively long delays
2112 # before noticing they should exit, but they should exit on their own.
2113 threads = [t for t in threading.enumerate()
2114 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002115 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002116 log_str = ""
2117 for thread_id, stack_frame in sys._current_frames().items():
2118 log_str += "Thread: %s\n" % thread_id
2119 log_str += "".join(traceback.format_stack(stack_frame))
2120 self.log.debug(log_str)
2121 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002122
James E. Blaira002b032017-04-18 10:35:48 -07002123 def assertCleanShutdown(self):
2124 pass
2125
James E. Blairc4ba97a2017-04-19 16:26:24 -07002126 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002127 parts = project.split('/')
2128 path = os.path.join(self.upstream_root, *parts[:-1])
2129 if not os.path.exists(path):
2130 os.makedirs(path)
2131 path = os.path.join(self.upstream_root, project)
2132 repo = git.Repo.init(path)
2133
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002134 with repo.config_writer() as config_writer:
2135 config_writer.set_value('user', 'email', 'user@example.com')
2136 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002137
Clark Boylanb640e052014-04-03 16:41:46 -07002138 repo.index.commit('initial commit')
2139 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002140 if tag:
2141 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002142
James E. Blair97d902e2014-08-21 13:25:56 -07002143 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002144 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002145 repo.git.clean('-x', '-f', '-d')
2146
James E. Blair97d902e2014-08-21 13:25:56 -07002147 def create_branch(self, project, branch):
2148 path = os.path.join(self.upstream_root, project)
2149 repo = git.Repo.init(path)
2150 fn = os.path.join(path, 'README')
2151
2152 branch_head = repo.create_head(branch)
2153 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002154 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002155 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002156 f.close()
2157 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002158 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002159
James E. Blair97d902e2014-08-21 13:25:56 -07002160 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002161 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002162 repo.git.clean('-x', '-f', '-d')
2163
Sachi King9f16d522016-03-16 12:20:45 +11002164 def create_commit(self, project):
2165 path = os.path.join(self.upstream_root, project)
2166 repo = git.Repo(path)
2167 repo.head.reference = repo.heads['master']
2168 file_name = os.path.join(path, 'README')
2169 with open(file_name, 'a') as f:
2170 f.write('creating fake commit\n')
2171 repo.index.add([file_name])
2172 commit = repo.index.commit('Creating a fake commit')
2173 return commit.hexsha
2174
James E. Blairf4a5f022017-04-18 14:01:10 -07002175 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002176 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002177 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002178 while len(self.builds):
2179 self.release(self.builds[0])
2180 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002181 i += 1
2182 if count is not None and i >= count:
2183 break
James E. Blairb8c16472015-05-05 14:55:26 -07002184
Clark Boylanb640e052014-04-03 16:41:46 -07002185 def release(self, job):
2186 if isinstance(job, FakeBuild):
2187 job.release()
2188 else:
2189 job.waiting = False
2190 self.log.debug("Queued job %s released" % job.unique)
2191 self.gearman_server.wakeConnections()
2192
2193 def getParameter(self, job, name):
2194 if isinstance(job, FakeBuild):
2195 return job.parameters[name]
2196 else:
2197 parameters = json.loads(job.arguments)
2198 return parameters[name]
2199
Clark Boylanb640e052014-04-03 16:41:46 -07002200 def haveAllBuildsReported(self):
2201 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002202 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002203 return False
2204 # Find out if every build that the worker has completed has been
2205 # reported back to Zuul. If it hasn't then that means a Gearman
2206 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002207 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002208 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002209 if not zbuild:
2210 # It has already been reported
2211 continue
2212 # It hasn't been reported yet.
2213 return False
2214 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002215 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002216 if connection.state == 'GRAB_WAIT':
2217 return False
2218 return True
2219
2220 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002221 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002222 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002223 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002224 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002225 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002226 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002227 for j in conn.related_jobs.values():
2228 if j.unique == build.uuid:
2229 client_job = j
2230 break
2231 if not client_job:
2232 self.log.debug("%s is not known to the gearman client" %
2233 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002234 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002235 if not client_job.handle:
2236 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002237 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002238 server_job = self.gearman_server.jobs.get(client_job.handle)
2239 if not server_job:
2240 self.log.debug("%s is not known to the gearman server" %
2241 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002242 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002243 if not hasattr(server_job, 'waiting'):
2244 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002245 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002246 if server_job.waiting:
2247 continue
James E. Blair17302972016-08-10 16:11:42 -07002248 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002249 self.log.debug("%s has not reported start" % build)
2250 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002251 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002252 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002253 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002254 if worker_build:
2255 if worker_build.isWaiting():
2256 continue
2257 else:
2258 self.log.debug("%s is running" % worker_build)
2259 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002260 else:
James E. Blair962220f2016-08-03 11:22:38 -07002261 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002262 return False
James E. Blaira002b032017-04-18 10:35:48 -07002263 for (build_uuid, job_worker) in \
2264 self.executor_server.job_workers.items():
2265 if build_uuid not in seen_builds:
2266 self.log.debug("%s is not finalized" % build_uuid)
2267 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002268 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002269
James E. Blairdce6cea2016-12-20 16:45:32 -08002270 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002271 if self.fake_nodepool.paused:
2272 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002273 if self.sched.nodepool.requests:
2274 return False
2275 return True
2276
Jan Hruban6b71aff2015-10-22 16:58:08 +02002277 def eventQueuesEmpty(self):
2278 for queue in self.event_queues:
2279 yield queue.empty()
2280
2281 def eventQueuesJoin(self):
2282 for queue in self.event_queues:
2283 queue.join()
2284
Clark Boylanb640e052014-04-03 16:41:46 -07002285 def waitUntilSettled(self):
2286 self.log.debug("Waiting until settled...")
2287 start = time.time()
2288 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002289 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002290 self.log.error("Timeout waiting for Zuul to settle")
2291 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002292 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002293 self.log.error(" %s: %s" % (queue, queue.empty()))
2294 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002295 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002296 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002297 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002298 self.log.error("All requests completed: %s" %
2299 (self.areAllNodeRequestsComplete(),))
2300 self.log.error("Merge client jobs: %s" %
2301 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002302 raise Exception("Timeout waiting for Zuul to settle")
2303 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002304
Paul Belanger174a8272017-03-14 13:20:10 -04002305 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002306 # have all build states propogated to zuul?
2307 if self.haveAllBuildsReported():
2308 # Join ensures that the queue is empty _and_ events have been
2309 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002310 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002311 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002312 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002313 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002314 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002315 self.areAllNodeRequestsComplete() and
2316 all(self.eventQueuesEmpty())):
2317 # The queue empty check is placed at the end to
2318 # ensure that if a component adds an event between
2319 # when locked the run handler and checked that the
2320 # components were stable, we don't erroneously
2321 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002322 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002323 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002324 self.log.debug("...settled.")
2325 return
2326 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002327 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002328 self.sched.wake_event.wait(0.1)
2329
2330 def countJobResults(self, jobs, result):
2331 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002332 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002333
James E. Blair96c6bf82016-01-15 16:20:40 -08002334 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002335 for job in self.history:
2336 if (job.name == name and
2337 (project is None or
2338 job.parameters['ZUUL_PROJECT'] == project)):
2339 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002340 raise Exception("Unable to find job %s in history" % name)
2341
2342 def assertEmptyQueues(self):
2343 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002344 for tenant in self.sched.abide.tenants.values():
2345 for pipeline in tenant.layout.pipelines.values():
2346 for queue in pipeline.queues:
2347 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002348 print('pipeline %s queue %s contents %s' % (
2349 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002350 self.assertEqual(len(queue.queue), 0,
2351 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002352
2353 def assertReportedStat(self, key, value=None, kind=None):
2354 start = time.time()
2355 while time.time() < (start + 5):
2356 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002357 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002358 if key == k:
2359 if value is None and kind is None:
2360 return
2361 elif value:
2362 if value == v:
2363 return
2364 elif kind:
2365 if v.endswith('|' + kind):
2366 return
2367 time.sleep(0.1)
2368
Clark Boylanb640e052014-04-03 16:41:46 -07002369 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002370
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002371 def assertBuilds(self, builds):
2372 """Assert that the running builds are as described.
2373
2374 The list of running builds is examined and must match exactly
2375 the list of builds described by the input.
2376
2377 :arg list builds: A list of dictionaries. Each item in the
2378 list must match the corresponding build in the build
2379 history, and each element of the dictionary must match the
2380 corresponding attribute of the build.
2381
2382 """
James E. Blair3158e282016-08-19 09:34:11 -07002383 try:
2384 self.assertEqual(len(self.builds), len(builds))
2385 for i, d in enumerate(builds):
2386 for k, v in d.items():
2387 self.assertEqual(
2388 getattr(self.builds[i], k), v,
2389 "Element %i in builds does not match" % (i,))
2390 except Exception:
2391 for build in self.builds:
2392 self.log.error("Running build: %s" % build)
2393 else:
2394 self.log.error("No running builds")
2395 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002396
James E. Blairb536ecc2016-08-31 10:11:42 -07002397 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002398 """Assert that the completed builds are as described.
2399
2400 The list of completed builds is examined and must match
2401 exactly the list of builds described by the input.
2402
2403 :arg list history: A list of dictionaries. Each item in the
2404 list must match the corresponding build in the build
2405 history, and each element of the dictionary must match the
2406 corresponding attribute of the build.
2407
James E. Blairb536ecc2016-08-31 10:11:42 -07002408 :arg bool ordered: If true, the history must match the order
2409 supplied, if false, the builds are permitted to have
2410 arrived in any order.
2411
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002412 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002413 def matches(history_item, item):
2414 for k, v in item.items():
2415 if getattr(history_item, k) != v:
2416 return False
2417 return True
James E. Blair3158e282016-08-19 09:34:11 -07002418 try:
2419 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002420 if ordered:
2421 for i, d in enumerate(history):
2422 if not matches(self.history[i], d):
2423 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002424 "Element %i in history does not match %s" %
2425 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002426 else:
2427 unseen = self.history[:]
2428 for i, d in enumerate(history):
2429 found = False
2430 for unseen_item in unseen:
2431 if matches(unseen_item, d):
2432 found = True
2433 unseen.remove(unseen_item)
2434 break
2435 if not found:
2436 raise Exception("No match found for element %i "
2437 "in history" % (i,))
2438 if unseen:
2439 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002440 except Exception:
2441 for build in self.history:
2442 self.log.error("Completed build: %s" % build)
2443 else:
2444 self.log.error("No completed builds")
2445 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002446
James E. Blair6ac368c2016-12-22 18:07:20 -08002447 def printHistory(self):
2448 """Log the build history.
2449
2450 This can be useful during tests to summarize what jobs have
2451 completed.
2452
2453 """
2454 self.log.debug("Build history:")
2455 for build in self.history:
2456 self.log.debug(build)
2457
James E. Blair59fdbac2015-12-07 17:08:06 -08002458 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002459 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2460
James E. Blair9ea70072017-04-19 16:05:30 -07002461 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002462 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002463 if not os.path.exists(root):
2464 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002465 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2466 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002467- tenant:
2468 name: openstack
2469 source:
2470 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002471 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002472 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002473 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002474 - org/project
2475 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002476 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002477 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002478 self.config.set('zuul', 'tenant_config',
2479 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002480 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002481
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002482 def addCommitToRepo(self, project, message, files,
2483 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002484 path = os.path.join(self.upstream_root, project)
2485 repo = git.Repo(path)
2486 repo.head.reference = branch
2487 zuul.merger.merger.reset_repo_to_head(repo)
2488 for fn, content in files.items():
2489 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002490 try:
2491 os.makedirs(os.path.dirname(fn))
2492 except OSError:
2493 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002494 with open(fn, 'w') as f:
2495 f.write(content)
2496 repo.index.add([fn])
2497 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002498 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002499 repo.heads[branch].commit = commit
2500 repo.head.reference = branch
2501 repo.git.clean('-x', '-f', '-d')
2502 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002503 if tag:
2504 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002505 return before
2506
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002507 def commitConfigUpdate(self, project_name, source_name):
2508 """Commit an update to zuul.yaml
2509
2510 This overwrites the zuul.yaml in the specificed project with
2511 the contents specified.
2512
2513 :arg str project_name: The name of the project containing
2514 zuul.yaml (e.g., common-config)
2515
2516 :arg str source_name: The path to the file (underneath the
2517 test fixture directory) whose contents should be used to
2518 replace zuul.yaml.
2519 """
2520
2521 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002522 files = {}
2523 with open(source_path, 'r') as f:
2524 data = f.read()
2525 layout = yaml.safe_load(data)
2526 files['zuul.yaml'] = data
2527 for item in layout:
2528 if 'job' in item:
2529 jobname = item['job']['name']
2530 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002531 before = self.addCommitToRepo(
2532 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002533 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002534 return before
2535
James E. Blair7fc8daa2016-08-08 15:37:15 -07002536 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002537
James E. Blair7fc8daa2016-08-08 15:37:15 -07002538 """Inject a Fake (Gerrit) event.
2539
2540 This method accepts a JSON-encoded event and simulates Zuul
2541 having received it from Gerrit. It could (and should)
2542 eventually apply to any connection type, but is currently only
2543 used with Gerrit connections. The name of the connection is
2544 used to look up the corresponding server, and the event is
2545 simulated as having been received by all Zuul connections
2546 attached to that server. So if two Gerrit connections in Zuul
2547 are connected to the same Gerrit server, and you invoke this
2548 method specifying the name of one of them, the event will be
2549 received by both.
2550
2551 .. note::
2552
2553 "self.fake_gerrit.addEvent" calls should be migrated to
2554 this method.
2555
2556 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002557 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002558 :arg str event: The JSON-encoded event.
2559
2560 """
2561 specified_conn = self.connections.connections[connection]
2562 for conn in self.connections.connections.values():
2563 if (isinstance(conn, specified_conn.__class__) and
2564 specified_conn.server == conn.server):
2565 conn.addEvent(event)
2566
James E. Blair3f876d52016-07-22 13:07:14 -07002567
2568class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002569 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002570 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002571
Joshua Heskethd78b4482015-09-14 16:56:34 -06002572
2573class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002574 def setup_config(self):
2575 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002576 for section_name in self.config.sections():
2577 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2578 section_name, re.I)
2579 if not con_match:
2580 continue
2581
2582 if self.config.get(section_name, 'driver') == 'sql':
2583 f = MySQLSchemaFixture()
2584 self.useFixture(f)
2585 if (self.config.get(section_name, 'dburi') ==
2586 '$MYSQL_FIXTURE_DBURI$'):
2587 self.config.set(section_name, 'dburi', f.dburi)