blob: 2fbdb88b043bd8aa3c9d11b24ce5c02434be1656 [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():
93 return hashlib.sha1(str(random.random())).hexdigest()
94
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
James E. Blair8b5408c2016-08-08 15:37:46 -0700499 # TODOv3(jeblair): can this be removed?
Joshua Hesketh642824b2014-07-01 17:54:59 +1000500 if 'label' in action:
501 parts = action['label'].split('=')
Joshua Hesketh352264b2015-08-11 23:42:08 +1000502 change.addApproval(parts[0], parts[2], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000503
Clark Boylanb640e052014-04-03 16:41:46 -0700504 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000505
Clark Boylanb640e052014-04-03 16:41:46 -0700506 if 'submit' in action:
507 change.setMerged()
508 if message:
509 change.setReported()
510
511 def query(self, number):
512 change = self.changes.get(int(number))
513 if change:
514 return change.query()
515 return {}
516
James E. Blairc494d542014-08-06 09:23:52 -0700517 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700518 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700519 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800520 if query.startswith('change:'):
521 # Query a specific changeid
522 changeid = query[len('change:'):]
523 l = [change.query() for change in self.changes.values()
524 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700525 elif query.startswith('message:'):
526 # Query the content of a commit message
527 msg = query[len('message:'):].strip()
528 l = [change.query() for change in self.changes.values()
529 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800530 else:
531 # Query all open changes
532 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700533 return l
James E. Blairc494d542014-08-06 09:23:52 -0700534
Joshua Hesketh352264b2015-08-11 23:42:08 +1000535 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700536 pass
537
Joshua Hesketh352264b2015-08-11 23:42:08 +1000538 def getGitUrl(self, project):
539 return os.path.join(self.upstream_root, project.name)
540
Clark Boylanb640e052014-04-03 16:41:46 -0700541
Gregory Haynes4fc12542015-04-22 20:38:06 -0700542class GithubChangeReference(git.Reference):
543 _common_path_default = "refs/pull"
544 _points_to_commits_only = True
545
546
547class FakeGithubPullRequest(object):
548
549 def __init__(self, github, number, project, branch,
Jan Hruban570d01c2016-03-10 21:51:32 +0100550 subject, upstream_root, files=[], number_of_commits=1):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700551 """Creates a new PR with several commits.
552 Sends an event about opened PR."""
553 self.github = github
554 self.source = github
555 self.number = number
556 self.project = project
557 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100558 self.subject = subject
559 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700560 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100561 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700562 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100563 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100564 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700565 self.updated_at = None
566 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100567 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100568 self.merge_message = None
Gregory Haynes4fc12542015-04-22 20:38:06 -0700569 self._createPRRef()
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 addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700574 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100575 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100577 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700578
Jan Hruban570d01c2016-03-10 21:51:32 +0100579 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700580 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100581 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700582 self._updateTimeStamp()
Jan Hrubane252a732017-01-03 15:03:09 +0100583 self._clearStatuses()
Gregory Haynes4fc12542015-04-22 20:38:06 -0700584
585 def getPullRequestOpenedEvent(self):
586 return self._getPullRequestEvent('opened')
587
588 def getPullRequestSynchronizeEvent(self):
589 return self._getPullRequestEvent('synchronize')
590
591 def getPullRequestReopenedEvent(self):
592 return self._getPullRequestEvent('reopened')
593
594 def getPullRequestClosedEvent(self):
595 return self._getPullRequestEvent('closed')
596
597 def addComment(self, message):
598 self.comments.append(message)
599 self._updateTimeStamp()
600
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200601 def getCommentAddedEvent(self, text):
602 name = 'issue_comment'
603 data = {
604 'action': 'created',
605 'issue': {
606 'number': self.number
607 },
608 'comment': {
609 'body': text
610 },
611 'repository': {
612 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100613 },
614 'sender': {
615 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200616 }
617 }
618 return (name, data)
619
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800620 def getReviewAddedEvent(self, review):
621 name = 'pull_request_review'
622 data = {
623 'action': 'submitted',
624 'pull_request': {
625 'number': self.number,
626 'title': self.subject,
627 'updated_at': self.updated_at,
628 'base': {
629 'ref': self.branch,
630 'repo': {
631 'full_name': self.project
632 }
633 },
634 'head': {
635 'sha': self.head_sha
636 }
637 },
638 'review': {
639 'state': review
640 },
641 'repository': {
642 'full_name': self.project
643 },
644 'sender': {
645 'login': 'ghuser'
646 }
647 }
648 return (name, data)
649
Jan Hruban16ad31f2015-11-07 14:39:07 +0100650 def addLabel(self, name):
651 if name not in self.labels:
652 self.labels.append(name)
653 self._updateTimeStamp()
654 return self._getLabelEvent(name)
655
656 def removeLabel(self, name):
657 if name in self.labels:
658 self.labels.remove(name)
659 self._updateTimeStamp()
660 return self._getUnlabelEvent(name)
661
662 def _getLabelEvent(self, label):
663 name = 'pull_request'
664 data = {
665 'action': 'labeled',
666 'pull_request': {
667 'number': self.number,
668 'updated_at': self.updated_at,
669 'base': {
670 'ref': self.branch,
671 'repo': {
672 'full_name': self.project
673 }
674 },
675 'head': {
676 'sha': self.head_sha
677 }
678 },
679 'label': {
680 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100681 },
682 'sender': {
683 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100684 }
685 }
686 return (name, data)
687
688 def _getUnlabelEvent(self, label):
689 name = 'pull_request'
690 data = {
691 'action': 'unlabeled',
692 'pull_request': {
693 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100694 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100695 'updated_at': self.updated_at,
696 'base': {
697 'ref': self.branch,
698 'repo': {
699 'full_name': self.project
700 }
701 },
702 'head': {
703 'sha': self.head_sha
704 }
705 },
706 'label': {
707 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100708 },
709 'sender': {
710 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100711 }
712 }
713 return (name, data)
714
Gregory Haynes4fc12542015-04-22 20:38:06 -0700715 def _getRepo(self):
716 repo_path = os.path.join(self.upstream_root, self.project)
717 return git.Repo(repo_path)
718
719 def _createPRRef(self):
720 repo = self._getRepo()
721 GithubChangeReference.create(
722 repo, self._getPRReference(), 'refs/tags/init')
723
Jan Hruban570d01c2016-03-10 21:51:32 +0100724 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700725 repo = self._getRepo()
726 ref = repo.references[self._getPRReference()]
727 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100728 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100730 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700731 repo.head.reference = ref
732 zuul.merger.merger.reset_repo_to_head(repo)
733 repo.git.clean('-x', '-f', '-d')
734
Jan Hruban570d01c2016-03-10 21:51:32 +0100735 if files:
736 fn = files[0]
737 self.files = files
738 else:
739 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
740 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100741 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 fn = os.path.join(repo.working_dir, fn)
743 f = open(fn, 'w')
744 with open(fn, 'w') as f:
745 f.write("test %s %s\n" %
746 (self.branch, self.number))
747 repo.index.add([fn])
748
749 self.head_sha = repo.index.commit(msg).hexsha
750 repo.head.reference = 'master'
751 zuul.merger.merger.reset_repo_to_head(repo)
752 repo.git.clean('-x', '-f', '-d')
753 repo.heads['master'].checkout()
754
755 def _updateTimeStamp(self):
756 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
757
758 def getPRHeadSha(self):
759 repo = self._getRepo()
760 return repo.references[self._getPRReference()].commit.hexsha
761
Jan Hrubane252a732017-01-03 15:03:09 +0100762 def setStatus(self, state, url, description, context):
763 self.statuses[context] = {
764 'state': state,
765 'url': url,
766 'description': description
767 }
768
769 def _clearStatuses(self):
770 self.statuses = {}
771
Gregory Haynes4fc12542015-04-22 20:38:06 -0700772 def _getPRReference(self):
773 return '%s/head' % self.number
774
775 def _getPullRequestEvent(self, action):
776 name = 'pull_request'
777 data = {
778 'action': action,
779 'number': self.number,
780 'pull_request': {
781 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100782 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700783 'updated_at': self.updated_at,
784 'base': {
785 'ref': self.branch,
786 'repo': {
787 'full_name': self.project
788 }
789 },
790 'head': {
791 'sha': self.head_sha
792 }
Jan Hruban3b415922016-02-03 13:10:22 +0100793 },
794 'sender': {
795 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700796 }
797 }
798 return (name, data)
799
800
801class FakeGithubConnection(githubconnection.GithubConnection):
802 log = logging.getLogger("zuul.test.FakeGithubConnection")
803
804 def __init__(self, driver, connection_name, connection_config,
805 upstream_root=None):
806 super(FakeGithubConnection, self).__init__(driver, connection_name,
807 connection_config)
808 self.connection_name = connection_name
809 self.pr_number = 0
810 self.pull_requests = []
811 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100812 self.merge_failure = False
813 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814
Jan Hruban570d01c2016-03-10 21:51:32 +0100815 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 self.pr_number += 1
817 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100818 self, self.pr_number, project, branch, subject, self.upstream_root,
819 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700820 self.pull_requests.append(pull_request)
821 return pull_request
822
Wayne1a78c612015-06-11 17:14:13 -0700823 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
824 if not old_rev:
825 old_rev = '00000000000000000000000000000000'
826 if not new_rev:
827 new_rev = random_sha1()
828 name = 'push'
829 data = {
830 'ref': ref,
831 'before': old_rev,
832 'after': new_rev,
833 'repository': {
834 'full_name': project
835 }
836 }
837 return (name, data)
838
Gregory Haynes4fc12542015-04-22 20:38:06 -0700839 def emitEvent(self, event):
840 """Emulates sending the GitHub webhook event to the connection."""
841 port = self.webapp.server.socket.getsockname()[1]
842 name, data = event
843 payload = json.dumps(data)
844 headers = {'X-Github-Event': name}
845 req = urllib.request.Request(
846 'http://localhost:%s/connection/%s/payload'
847 % (port, self.connection_name),
848 data=payload, headers=headers)
849 urllib.request.urlopen(req)
850
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200851 def getPull(self, project, number):
852 pr = self.pull_requests[number - 1]
853 data = {
854 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100855 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200856 'updated_at': pr.updated_at,
857 'base': {
858 'repo': {
859 'full_name': pr.project
860 },
861 'ref': pr.branch,
862 },
Jan Hruban37615e52015-11-19 14:30:49 +0100863 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200864 'head': {
865 'sha': pr.head_sha
866 }
867 }
868 return data
869
Jan Hruban570d01c2016-03-10 21:51:32 +0100870 def getPullFileNames(self, project, number):
871 pr = self.pull_requests[number - 1]
872 return pr.files
873
Jan Hruban3b415922016-02-03 13:10:22 +0100874 def getUser(self, login):
875 data = {
876 'username': login,
877 'name': 'Github User',
878 'email': 'github.user@example.com'
879 }
880 return data
881
Gregory Haynes4fc12542015-04-22 20:38:06 -0700882 def getGitUrl(self, project):
883 return os.path.join(self.upstream_root, str(project))
884
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200885 def real_getGitUrl(self, project):
886 return super(FakeGithubConnection, self).getGitUrl(project)
887
Gregory Haynes4fc12542015-04-22 20:38:06 -0700888 def getProjectBranches(self, project):
889 """Masks getProjectBranches since we don't have a real github"""
890
891 # just returns master for now
892 return ['master']
893
Jan Hrubane252a732017-01-03 15:03:09 +0100894 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700895 pull_request = self.pull_requests[pr_number - 1]
896 pull_request.addComment(message)
897
Jan Hruban3b415922016-02-03 13:10:22 +0100898 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100899 pull_request = self.pull_requests[pr_number - 1]
900 if self.merge_failure:
901 raise Exception('Pull request was not merged')
902 if self.merge_not_allowed_count > 0:
903 self.merge_not_allowed_count -= 1
904 raise MergeFailure('Merge was not successful due to mergeability'
905 ' conflict')
906 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100907 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100908
Jan Hrubane252a732017-01-03 15:03:09 +0100909 def setCommitStatus(self, project, sha, state,
910 url='', description='', context=''):
911 owner, proj = project.split('/')
912 for pr in self.pull_requests:
913 pr_owner, pr_project = pr.project.split('/')
914 if (pr_owner == owner and pr_project == proj and
915 pr.head_sha == sha):
916 pr.setStatus(state, url, description, context)
917
Jan Hruban16ad31f2015-11-07 14:39:07 +0100918 def labelPull(self, project, pr_number, label):
919 pull_request = self.pull_requests[pr_number - 1]
920 pull_request.addLabel(label)
921
922 def unlabelPull(self, project, pr_number, label):
923 pull_request = self.pull_requests[pr_number - 1]
924 pull_request.removeLabel(label)
925
Gregory Haynes4fc12542015-04-22 20:38:06 -0700926
Clark Boylanb640e052014-04-03 16:41:46 -0700927class BuildHistory(object):
928 def __init__(self, **kw):
929 self.__dict__.update(kw)
930
931 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700932 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
933 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700934
935
936class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200937 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700938 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700939 self.url = url
940
941 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700942 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700943 path = res.path
944 project = '/'.join(path.split('/')[2:-2])
945 ret = '001e# service=git-upload-pack\n'
946 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
947 'multi_ack thin-pack side-band side-band-64k ofs-delta '
948 'shallow no-progress include-tag multi_ack_detailed no-done\n')
949 path = os.path.join(self.upstream_root, project)
950 repo = git.Repo(path)
951 for ref in repo.refs:
952 r = ref.object.hexsha + ' ' + ref.path + '\n'
953 ret += '%04x%s' % (len(r) + 4, r)
954 ret += '0000'
955 return ret
956
957
Clark Boylanb640e052014-04-03 16:41:46 -0700958class FakeStatsd(threading.Thread):
959 def __init__(self):
960 threading.Thread.__init__(self)
961 self.daemon = True
962 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
963 self.sock.bind(('', 0))
964 self.port = self.sock.getsockname()[1]
965 self.wake_read, self.wake_write = os.pipe()
966 self.stats = []
967
968 def run(self):
969 while True:
970 poll = select.poll()
971 poll.register(self.sock, select.POLLIN)
972 poll.register(self.wake_read, select.POLLIN)
973 ret = poll.poll()
974 for (fd, event) in ret:
975 if fd == self.sock.fileno():
976 data = self.sock.recvfrom(1024)
977 if not data:
978 return
979 self.stats.append(data[0])
980 if fd == self.wake_read:
981 return
982
983 def stop(self):
984 os.write(self.wake_write, '1\n')
985
986
James E. Blaire1767bc2016-08-02 10:00:27 -0700987class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700988 log = logging.getLogger("zuul.test")
989
Paul Belanger174a8272017-03-14 13:20:10 -0400990 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700991 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400992 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700993 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700994 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700995 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700996 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700997 # TODOv3(jeblair): self.node is really "the image of the node
998 # assigned". We should rename it (self.node_image?) if we
999 # keep using it like this, or we may end up exposing more of
1000 # the complexity around multi-node jobs here
1001 # (self.nodes[0].image?)
1002 self.node = None
1003 if len(self.parameters.get('nodes')) == 1:
1004 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -07001005 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001006 self.pipeline = self.parameters['ZUUL_PIPELINE']
1007 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -07001008 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001009 self.wait_condition = threading.Condition()
1010 self.waiting = False
1011 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001012 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001013 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001014 self.changes = None
1015 if 'ZUUL_CHANGE_IDS' in self.parameters:
1016 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -07001017
James E. Blair3158e282016-08-19 09:34:11 -07001018 def __repr__(self):
1019 waiting = ''
1020 if self.waiting:
1021 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001022 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1023 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001024
Clark Boylanb640e052014-04-03 16:41:46 -07001025 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001026 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001027 self.wait_condition.acquire()
1028 self.wait_condition.notify()
1029 self.waiting = False
1030 self.log.debug("Build %s released" % self.unique)
1031 self.wait_condition.release()
1032
1033 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001034 """Return whether this build is being held.
1035
1036 :returns: Whether the build is being held.
1037 :rtype: bool
1038 """
1039
Clark Boylanb640e052014-04-03 16:41:46 -07001040 self.wait_condition.acquire()
1041 if self.waiting:
1042 ret = True
1043 else:
1044 ret = False
1045 self.wait_condition.release()
1046 return ret
1047
1048 def _wait(self):
1049 self.wait_condition.acquire()
1050 self.waiting = True
1051 self.log.debug("Build %s waiting" % self.unique)
1052 self.wait_condition.wait()
1053 self.wait_condition.release()
1054
1055 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001056 self.log.debug('Running build %s' % self.unique)
1057
Paul Belanger174a8272017-03-14 13:20:10 -04001058 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001059 self.log.debug('Holding build %s' % self.unique)
1060 self._wait()
1061 self.log.debug("Build %s continuing" % self.unique)
1062
James E. Blair412fba82017-01-26 15:00:50 -08001063 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001064 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001065 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001066 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001067 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001068 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001069 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001070
James E. Blaire1767bc2016-08-02 10:00:27 -07001071 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001072
James E. Blaira5dba232016-08-08 15:53:24 -07001073 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001074 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001075 for change in changes:
1076 if self.hasChanges(change):
1077 return True
1078 return False
1079
James E. Blaire7b99a02016-08-05 14:27:34 -07001080 def hasChanges(self, *changes):
1081 """Return whether this build has certain changes in its git repos.
1082
1083 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001084 are expected to be present (in order) in the git repository of
1085 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001086
1087 :returns: Whether the build has the indicated changes.
1088 :rtype: bool
1089
1090 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001091 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001092 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001093 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001094 try:
1095 repo = git.Repo(path)
1096 except NoSuchPathError as e:
1097 self.log.debug('%s' % e)
1098 return False
1099 ref = self.parameters['ZUUL_REF']
1100 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1101 commit_message = '%s-1' % change.subject
1102 self.log.debug("Checking if build %s has changes; commit_message "
1103 "%s; repo_messages %s" % (self, commit_message,
1104 repo_messages))
1105 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001106 self.log.debug(" messages do not match")
1107 return False
1108 self.log.debug(" OK")
1109 return True
1110
Clark Boylanb640e052014-04-03 16:41:46 -07001111
Paul Belanger174a8272017-03-14 13:20:10 -04001112class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1113 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001114
Paul Belanger174a8272017-03-14 13:20:10 -04001115 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001116 they will report that they have started but then pause until
1117 released before reporting completion. This attribute may be
1118 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001119 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001120 be explicitly released.
1121
1122 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001123 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001124 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001125 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001126 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001127 self.hold_jobs_in_build = False
1128 self.lock = threading.Lock()
1129 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001130 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001131 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001132 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001133
James E. Blaira5dba232016-08-08 15:53:24 -07001134 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001135 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001136
1137 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001138 :arg Change change: The :py:class:`~tests.base.FakeChange`
1139 instance which should cause the job to fail. This job
1140 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001141
1142 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001143 l = self.fail_tests.get(name, [])
1144 l.append(change)
1145 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001146
James E. Blair962220f2016-08-03 11:22:38 -07001147 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001148 """Release a held build.
1149
1150 :arg str regex: A regular expression which, if supplied, will
1151 cause only builds with matching names to be released. If
1152 not supplied, all builds will be released.
1153
1154 """
James E. Blair962220f2016-08-03 11:22:38 -07001155 builds = self.running_builds[:]
1156 self.log.debug("Releasing build %s (%s)" % (regex,
1157 len(self.running_builds)))
1158 for build in builds:
1159 if not regex or re.match(regex, build.name):
1160 self.log.debug("Releasing build %s" %
1161 (build.parameters['ZUUL_UUID']))
1162 build.release()
1163 else:
1164 self.log.debug("Not releasing build %s" %
1165 (build.parameters['ZUUL_UUID']))
1166 self.log.debug("Done releasing builds %s (%s)" %
1167 (regex, len(self.running_builds)))
1168
Paul Belanger174a8272017-03-14 13:20:10 -04001169 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001170 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001171 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001172 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001173 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001174 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001175 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001176 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001177 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1178 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001179
1180 def stopJob(self, job):
1181 self.log.debug("handle stop")
1182 parameters = json.loads(job.arguments)
1183 uuid = parameters['uuid']
1184 for build in self.running_builds:
1185 if build.unique == uuid:
1186 build.aborted = True
1187 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001188 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001189
James E. Blaira002b032017-04-18 10:35:48 -07001190 def stop(self):
1191 for build in self.running_builds:
1192 build.release()
1193 super(RecordingExecutorServer, self).stop()
1194
Joshua Hesketh50c21782016-10-13 21:34:14 +11001195
Paul Belanger174a8272017-03-14 13:20:10 -04001196class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001197 def doMergeChanges(self, items):
1198 # Get a merger in order to update the repos involved in this job.
1199 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1200 if not commit: # merge conflict
1201 self.recordResult('MERGER_FAILURE')
1202 return commit
1203
1204 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001205 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001206 self.executor_server.lock.acquire()
1207 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001208 BuildHistory(name=build.name, result=result, changes=build.changes,
1209 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001210 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001211 pipeline=build.parameters['ZUUL_PIPELINE'])
1212 )
Paul Belanger174a8272017-03-14 13:20:10 -04001213 self.executor_server.running_builds.remove(build)
1214 del self.executor_server.job_builds[self.job.unique]
1215 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001216
1217 def runPlaybooks(self, args):
1218 build = self.executor_server.job_builds[self.job.unique]
1219 build.jobdir = self.jobdir
1220
1221 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1222 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001223 return result
1224
Monty Taylore6562aa2017-02-20 07:37:39 -05001225 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001226 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001227
Paul Belanger174a8272017-03-14 13:20:10 -04001228 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001229 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001230 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001231 else:
1232 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001233 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001234
James E. Blairad8dca02017-02-21 11:48:32 -05001235 def getHostList(self, args):
1236 self.log.debug("hostlist")
1237 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001238 for host in hosts:
1239 host['host_vars']['ansible_connection'] = 'local'
1240
1241 hosts.append(dict(
1242 name='localhost',
1243 host_vars=dict(ansible_connection='local'),
1244 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001245 return hosts
1246
James E. Blairf5dbd002015-12-23 15:26:17 -08001247
Clark Boylanb640e052014-04-03 16:41:46 -07001248class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001249 """A Gearman server for use in tests.
1250
1251 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1252 added to the queue but will not be distributed to workers
1253 until released. This attribute may be changed at any time and
1254 will take effect for subsequently enqueued jobs, but
1255 previously held jobs will still need to be explicitly
1256 released.
1257
1258 """
1259
Clark Boylanb640e052014-04-03 16:41:46 -07001260 def __init__(self):
1261 self.hold_jobs_in_queue = False
1262 super(FakeGearmanServer, self).__init__(0)
1263
1264 def getJobForConnection(self, connection, peek=False):
1265 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1266 for job in queue:
1267 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001268 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001269 job.waiting = self.hold_jobs_in_queue
1270 else:
1271 job.waiting = False
1272 if job.waiting:
1273 continue
1274 if job.name in connection.functions:
1275 if not peek:
1276 queue.remove(job)
1277 connection.related_jobs[job.handle] = job
1278 job.worker_connection = connection
1279 job.running = True
1280 return job
1281 return None
1282
1283 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001284 """Release a held job.
1285
1286 :arg str regex: A regular expression which, if supplied, will
1287 cause only jobs with matching names to be released. If
1288 not supplied, all jobs will be released.
1289 """
Clark Boylanb640e052014-04-03 16:41:46 -07001290 released = False
1291 qlen = (len(self.high_queue) + len(self.normal_queue) +
1292 len(self.low_queue))
1293 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1294 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001295 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001296 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001297 parameters = json.loads(job.arguments)
1298 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001299 self.log.debug("releasing queued job %s" %
1300 job.unique)
1301 job.waiting = False
1302 released = True
1303 else:
1304 self.log.debug("not releasing queued job %s" %
1305 job.unique)
1306 if released:
1307 self.wakeConnections()
1308 qlen = (len(self.high_queue) + len(self.normal_queue) +
1309 len(self.low_queue))
1310 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1311
1312
1313class FakeSMTP(object):
1314 log = logging.getLogger('zuul.FakeSMTP')
1315
1316 def __init__(self, messages, server, port):
1317 self.server = server
1318 self.port = port
1319 self.messages = messages
1320
1321 def sendmail(self, from_email, to_email, msg):
1322 self.log.info("Sending email from %s, to %s, with msg %s" % (
1323 from_email, to_email, msg))
1324
1325 headers = msg.split('\n\n', 1)[0]
1326 body = msg.split('\n\n', 1)[1]
1327
1328 self.messages.append(dict(
1329 from_email=from_email,
1330 to_email=to_email,
1331 msg=msg,
1332 headers=headers,
1333 body=body,
1334 ))
1335
1336 return True
1337
1338 def quit(self):
1339 return True
1340
1341
James E. Blairdce6cea2016-12-20 16:45:32 -08001342class FakeNodepool(object):
1343 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001344 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001345
1346 log = logging.getLogger("zuul.test.FakeNodepool")
1347
1348 def __init__(self, host, port, chroot):
1349 self.client = kazoo.client.KazooClient(
1350 hosts='%s:%s%s' % (host, port, chroot))
1351 self.client.start()
1352 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001353 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001354 self.thread = threading.Thread(target=self.run)
1355 self.thread.daemon = True
1356 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001357 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001358
1359 def stop(self):
1360 self._running = False
1361 self.thread.join()
1362 self.client.stop()
1363 self.client.close()
1364
1365 def run(self):
1366 while self._running:
1367 self._run()
1368 time.sleep(0.1)
1369
1370 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001371 if self.paused:
1372 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001373 for req in self.getNodeRequests():
1374 self.fulfillRequest(req)
1375
1376 def getNodeRequests(self):
1377 try:
1378 reqids = self.client.get_children(self.REQUEST_ROOT)
1379 except kazoo.exceptions.NoNodeError:
1380 return []
1381 reqs = []
1382 for oid in sorted(reqids):
1383 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001384 try:
1385 data, stat = self.client.get(path)
1386 data = json.loads(data)
1387 data['_oid'] = oid
1388 reqs.append(data)
1389 except kazoo.exceptions.NoNodeError:
1390 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001391 return reqs
1392
James E. Blaire18d4602017-01-05 11:17:28 -08001393 def getNodes(self):
1394 try:
1395 nodeids = self.client.get_children(self.NODE_ROOT)
1396 except kazoo.exceptions.NoNodeError:
1397 return []
1398 nodes = []
1399 for oid in sorted(nodeids):
1400 path = self.NODE_ROOT + '/' + oid
1401 data, stat = self.client.get(path)
1402 data = json.loads(data)
1403 data['_oid'] = oid
1404 try:
1405 lockfiles = self.client.get_children(path + '/lock')
1406 except kazoo.exceptions.NoNodeError:
1407 lockfiles = []
1408 if lockfiles:
1409 data['_lock'] = True
1410 else:
1411 data['_lock'] = False
1412 nodes.append(data)
1413 return nodes
1414
James E. Blaira38c28e2017-01-04 10:33:20 -08001415 def makeNode(self, request_id, node_type):
1416 now = time.time()
1417 path = '/nodepool/nodes/'
1418 data = dict(type=node_type,
1419 provider='test-provider',
1420 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001421 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001422 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001423 public_ipv4='127.0.0.1',
1424 private_ipv4=None,
1425 public_ipv6=None,
1426 allocated_to=request_id,
1427 state='ready',
1428 state_time=now,
1429 created_time=now,
1430 updated_time=now,
1431 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001432 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001433 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001434 data = json.dumps(data)
1435 path = self.client.create(path, data,
1436 makepath=True,
1437 sequence=True)
1438 nodeid = path.split("/")[-1]
1439 return nodeid
1440
James E. Blair6ab79e02017-01-06 10:10:17 -08001441 def addFailRequest(self, request):
1442 self.fail_requests.add(request['_oid'])
1443
James E. Blairdce6cea2016-12-20 16:45:32 -08001444 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001445 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001446 return
1447 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001448 oid = request['_oid']
1449 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001450
James E. Blair6ab79e02017-01-06 10:10:17 -08001451 if oid in self.fail_requests:
1452 request['state'] = 'failed'
1453 else:
1454 request['state'] = 'fulfilled'
1455 nodes = []
1456 for node in request['node_types']:
1457 nodeid = self.makeNode(oid, node)
1458 nodes.append(nodeid)
1459 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001460
James E. Blaira38c28e2017-01-04 10:33:20 -08001461 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001462 path = self.REQUEST_ROOT + '/' + oid
1463 data = json.dumps(request)
1464 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1465 self.client.set(path, data)
1466
1467
James E. Blair498059b2016-12-20 13:50:13 -08001468class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001469 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001470 super(ChrootedKazooFixture, self).__init__()
1471
1472 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1473 if ':' in zk_host:
1474 host, port = zk_host.split(':')
1475 else:
1476 host = zk_host
1477 port = None
1478
1479 self.zookeeper_host = host
1480
1481 if not port:
1482 self.zookeeper_port = 2181
1483 else:
1484 self.zookeeper_port = int(port)
1485
Clark Boylan621ec9a2017-04-07 17:41:33 -07001486 self.test_id = test_id
1487
James E. Blair498059b2016-12-20 13:50:13 -08001488 def _setUp(self):
1489 # Make sure the test chroot paths do not conflict
1490 random_bits = ''.join(random.choice(string.ascii_lowercase +
1491 string.ascii_uppercase)
1492 for x in range(8))
1493
Clark Boylan621ec9a2017-04-07 17:41:33 -07001494 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001495 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1496
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001497 self.addCleanup(self._cleanup)
1498
James E. Blair498059b2016-12-20 13:50:13 -08001499 # Ensure the chroot path exists and clean up any pre-existing znodes.
1500 _tmp_client = kazoo.client.KazooClient(
1501 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1502 _tmp_client.start()
1503
1504 if _tmp_client.exists(self.zookeeper_chroot):
1505 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1506
1507 _tmp_client.ensure_path(self.zookeeper_chroot)
1508 _tmp_client.stop()
1509 _tmp_client.close()
1510
James E. Blair498059b2016-12-20 13:50:13 -08001511 def _cleanup(self):
1512 '''Remove the chroot path.'''
1513 # Need a non-chroot'ed client to remove the chroot path
1514 _tmp_client = kazoo.client.KazooClient(
1515 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1516 _tmp_client.start()
1517 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1518 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001519 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001520
1521
Joshua Heskethd78b4482015-09-14 16:56:34 -06001522class MySQLSchemaFixture(fixtures.Fixture):
1523 def setUp(self):
1524 super(MySQLSchemaFixture, self).setUp()
1525
1526 random_bits = ''.join(random.choice(string.ascii_lowercase +
1527 string.ascii_uppercase)
1528 for x in range(8))
1529 self.name = '%s_%s' % (random_bits, os.getpid())
1530 self.passwd = uuid.uuid4().hex
1531 db = pymysql.connect(host="localhost",
1532 user="openstack_citest",
1533 passwd="openstack_citest",
1534 db="openstack_citest")
1535 cur = db.cursor()
1536 cur.execute("create database %s" % self.name)
1537 cur.execute(
1538 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1539 (self.name, self.name, self.passwd))
1540 cur.execute("flush privileges")
1541
1542 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1543 self.passwd,
1544 self.name)
1545 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1546 self.addCleanup(self.cleanup)
1547
1548 def cleanup(self):
1549 db = pymysql.connect(host="localhost",
1550 user="openstack_citest",
1551 passwd="openstack_citest",
1552 db="openstack_citest")
1553 cur = db.cursor()
1554 cur.execute("drop database %s" % self.name)
1555 cur.execute("drop user '%s'@'localhost'" % self.name)
1556 cur.execute("flush privileges")
1557
1558
Maru Newby3fe5f852015-01-13 04:22:14 +00001559class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001560 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001561 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001562
James E. Blair1c236df2017-02-01 14:07:24 -08001563 def attachLogs(self, *args):
1564 def reader():
1565 self._log_stream.seek(0)
1566 while True:
1567 x = self._log_stream.read(4096)
1568 if not x:
1569 break
1570 yield x.encode('utf8')
1571 content = testtools.content.content_from_reader(
1572 reader,
1573 testtools.content_type.UTF8_TEXT,
1574 False)
1575 self.addDetail('logging', content)
1576
Clark Boylanb640e052014-04-03 16:41:46 -07001577 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001578 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001579 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1580 try:
1581 test_timeout = int(test_timeout)
1582 except ValueError:
1583 # If timeout value is invalid do not set a timeout.
1584 test_timeout = 0
1585 if test_timeout > 0:
1586 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1587
1588 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1589 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1590 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1591 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1592 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1593 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1594 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1595 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1596 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1597 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001598 self._log_stream = StringIO()
1599 self.addOnException(self.attachLogs)
1600 else:
1601 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001602
James E. Blair1c236df2017-02-01 14:07:24 -08001603 handler = logging.StreamHandler(self._log_stream)
1604 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1605 '%(levelname)-8s %(message)s')
1606 handler.setFormatter(formatter)
1607
1608 logger = logging.getLogger()
1609 logger.setLevel(logging.DEBUG)
1610 logger.addHandler(handler)
1611
Clark Boylan3410d532017-04-25 12:35:29 -07001612 # Make sure we don't carry old handlers around in process state
1613 # which slows down test runs
1614 self.addCleanup(logger.removeHandler, handler)
1615 self.addCleanup(handler.close)
1616 self.addCleanup(handler.flush)
1617
James E. Blair1c236df2017-02-01 14:07:24 -08001618 # NOTE(notmorgan): Extract logging overrides for specific
1619 # libraries from the OS_LOG_DEFAULTS env and create loggers
1620 # for each. This is used to limit the output during test runs
1621 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001622 log_defaults_from_env = os.environ.get(
1623 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001624 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001625
James E. Blairdce6cea2016-12-20 16:45:32 -08001626 if log_defaults_from_env:
1627 for default in log_defaults_from_env.split(','):
1628 try:
1629 name, level_str = default.split('=', 1)
1630 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001631 logger = logging.getLogger(name)
1632 logger.setLevel(level)
1633 logger.addHandler(handler)
1634 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001635 except ValueError:
1636 # NOTE(notmorgan): Invalid format of the log default,
1637 # skip and don't try and apply a logger for the
1638 # specified module
1639 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001640
Maru Newby3fe5f852015-01-13 04:22:14 +00001641
1642class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001643 """A test case with a functioning Zuul.
1644
1645 The following class variables are used during test setup and can
1646 be overidden by subclasses but are effectively read-only once a
1647 test method starts running:
1648
1649 :cvar str config_file: This points to the main zuul config file
1650 within the fixtures directory. Subclasses may override this
1651 to obtain a different behavior.
1652
1653 :cvar str tenant_config_file: This is the tenant config file
1654 (which specifies from what git repos the configuration should
1655 be loaded). It defaults to the value specified in
1656 `config_file` but can be overidden by subclasses to obtain a
1657 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001658 configuration. See also the :py:func:`simple_layout`
1659 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001660
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001661 :cvar bool create_project_keys: Indicates whether Zuul should
1662 auto-generate keys for each project, or whether the test
1663 infrastructure should insert dummy keys to save time during
1664 startup. Defaults to False.
1665
James E. Blaire7b99a02016-08-05 14:27:34 -07001666 The following are instance variables that are useful within test
1667 methods:
1668
1669 :ivar FakeGerritConnection fake_<connection>:
1670 A :py:class:`~tests.base.FakeGerritConnection` will be
1671 instantiated for each connection present in the config file
1672 and stored here. For instance, `fake_gerrit` will hold the
1673 FakeGerritConnection object for a connection named `gerrit`.
1674
1675 :ivar FakeGearmanServer gearman_server: An instance of
1676 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1677 server that all of the Zuul components in this test use to
1678 communicate with each other.
1679
Paul Belanger174a8272017-03-14 13:20:10 -04001680 :ivar RecordingExecutorServer executor_server: An instance of
1681 :py:class:`~tests.base.RecordingExecutorServer` which is the
1682 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001683
1684 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1685 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001686 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001687 list upon completion.
1688
1689 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1690 objects representing completed builds. They are appended to
1691 the list in the order they complete.
1692
1693 """
1694
James E. Blair83005782015-12-11 14:46:03 -08001695 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001696 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001697 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001698
1699 def _startMerger(self):
1700 self.merge_server = zuul.merger.server.MergeServer(self.config,
1701 self.connections)
1702 self.merge_server.start()
1703
Maru Newby3fe5f852015-01-13 04:22:14 +00001704 def setUp(self):
1705 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001706
1707 self.setupZK()
1708
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001709 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001710 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001711 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1712 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001713 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001714 tmp_root = tempfile.mkdtemp(
1715 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001716 self.test_root = os.path.join(tmp_root, "zuul-test")
1717 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001718 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001719 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001720 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001721
1722 if os.path.exists(self.test_root):
1723 shutil.rmtree(self.test_root)
1724 os.makedirs(self.test_root)
1725 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001726 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001727
1728 # Make per test copy of Configuration.
1729 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001730 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001731 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001732 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001733 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001734 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001735 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001736
Clark Boylanb640e052014-04-03 16:41:46 -07001737 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001738 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1739 # see: https://github.com/jsocol/pystatsd/issues/61
1740 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001741 os.environ['STATSD_PORT'] = str(self.statsd.port)
1742 self.statsd.start()
1743 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001744 reload_module(statsd)
1745 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001746
1747 self.gearman_server = FakeGearmanServer()
1748
1749 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001750 self.log.info("Gearman server on port %s" %
1751 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001752
James E. Blaire511d2f2016-12-08 15:22:26 -08001753 gerritsource.GerritSource.replication_timeout = 1.5
1754 gerritsource.GerritSource.replication_retry_interval = 0.5
1755 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001756
Joshua Hesketh352264b2015-08-11 23:42:08 +10001757 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001758
Jan Hruban7083edd2015-08-21 14:00:54 +02001759 self.webapp = zuul.webapp.WebApp(
1760 self.sched, port=0, listen_address='127.0.0.1')
1761
Jan Hruban6b71aff2015-10-22 16:58:08 +02001762 self.event_queues = [
1763 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001764 self.sched.trigger_event_queue,
1765 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001766 ]
1767
James E. Blairfef78942016-03-11 16:28:56 -08001768 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001769 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001770
Clark Boylanb640e052014-04-03 16:41:46 -07001771 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001772 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001773 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001774 return FakeURLOpener(self.upstream_root, *args, **kw)
1775
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001776 old_urlopen = urllib.request.urlopen
1777 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001778
James E. Blair3f876d52016-07-22 13:07:14 -07001779 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001780
Paul Belanger174a8272017-03-14 13:20:10 -04001781 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001782 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001783 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001784 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001785 _test_root=self.test_root,
1786 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001787 self.executor_server.start()
1788 self.history = self.executor_server.build_history
1789 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001790
Paul Belanger174a8272017-03-14 13:20:10 -04001791 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001792 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001793 self.merge_client = zuul.merger.client.MergeClient(
1794 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001795 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001796 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001797 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001798
James E. Blair0d5a36e2017-02-21 10:53:44 -05001799 self.fake_nodepool = FakeNodepool(
1800 self.zk_chroot_fixture.zookeeper_host,
1801 self.zk_chroot_fixture.zookeeper_port,
1802 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001803
Paul Belanger174a8272017-03-14 13:20:10 -04001804 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001805 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001806 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001807 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001808
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001809 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001810
1811 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001812 self.webapp.start()
1813 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001814 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001815 # Cleanups are run in reverse order
1816 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001817 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001818 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001819
James E. Blairb9c0d772017-03-03 14:34:49 -08001820 self.sched.reconfigure(self.config)
1821 self.sched.resume()
1822
James E. Blairfef78942016-03-11 16:28:56 -08001823 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001824 # Set up gerrit related fakes
1825 # Set a changes database so multiple FakeGerrit's can report back to
1826 # a virtual canonical database given by the configured hostname
1827 self.gerrit_changes_dbs = {}
1828
1829 def getGerritConnection(driver, name, config):
1830 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1831 con = FakeGerritConnection(driver, name, config,
1832 changes_db=db,
1833 upstream_root=self.upstream_root)
1834 self.event_queues.append(con.event_queue)
1835 setattr(self, 'fake_' + name, con)
1836 return con
1837
1838 self.useFixture(fixtures.MonkeyPatch(
1839 'zuul.driver.gerrit.GerritDriver.getConnection',
1840 getGerritConnection))
1841
Gregory Haynes4fc12542015-04-22 20:38:06 -07001842 def getGithubConnection(driver, name, config):
1843 con = FakeGithubConnection(driver, name, config,
1844 upstream_root=self.upstream_root)
1845 setattr(self, 'fake_' + name, con)
1846 return con
1847
1848 self.useFixture(fixtures.MonkeyPatch(
1849 'zuul.driver.github.GithubDriver.getConnection',
1850 getGithubConnection))
1851
James E. Blaire511d2f2016-12-08 15:22:26 -08001852 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001853 # TODO(jhesketh): This should come from lib.connections for better
1854 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001855 # Register connections from the config
1856 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001857
Joshua Hesketh352264b2015-08-11 23:42:08 +10001858 def FakeSMTPFactory(*args, **kw):
1859 args = [self.smtp_messages] + list(args)
1860 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001861
Joshua Hesketh352264b2015-08-11 23:42:08 +10001862 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001863
James E. Blaire511d2f2016-12-08 15:22:26 -08001864 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001865 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001866 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001867
James E. Blair83005782015-12-11 14:46:03 -08001868 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001869 # This creates the per-test configuration object. It can be
1870 # overriden by subclasses, but should not need to be since it
1871 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001872 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001873 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001874
1875 if not self.setupSimpleLayout():
1876 if hasattr(self, 'tenant_config_file'):
1877 self.config.set('zuul', 'tenant_config',
1878 self.tenant_config_file)
1879 git_path = os.path.join(
1880 os.path.dirname(
1881 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1882 'git')
1883 if os.path.exists(git_path):
1884 for reponame in os.listdir(git_path):
1885 project = reponame.replace('_', '/')
1886 self.copyDirToRepo(project,
1887 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001888 self.setupAllProjectKeys()
1889
James E. Blair06cc3922017-04-19 10:08:10 -07001890 def setupSimpleLayout(self):
1891 # If the test method has been decorated with a simple_layout,
1892 # use that instead of the class tenant_config_file. Set up a
1893 # single config-project with the specified layout, and
1894 # initialize repos for all of the 'project' entries which
1895 # appear in the layout.
1896 test_name = self.id().split('.')[-1]
1897 test = getattr(self, test_name)
1898 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001899 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001900 else:
1901 return False
1902
James E. Blairb70e55a2017-04-19 12:57:02 -07001903 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001904 path = os.path.join(FIXTURE_DIR, path)
1905 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001906 data = f.read()
1907 layout = yaml.safe_load(data)
1908 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001909 untrusted_projects = []
1910 for item in layout:
1911 if 'project' in item:
1912 name = item['project']['name']
1913 untrusted_projects.append(name)
1914 self.init_repo(name)
1915 self.addCommitToRepo(name, 'initial commit',
1916 files={'README': ''},
1917 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001918 if 'job' in item:
1919 jobname = item['job']['name']
1920 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001921
1922 root = os.path.join(self.test_root, "config")
1923 if not os.path.exists(root):
1924 os.makedirs(root)
1925 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1926 config = [{'tenant':
1927 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001928 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001929 {'config-projects': ['common-config'],
1930 'untrusted-projects': untrusted_projects}}}}]
1931 f.write(yaml.dump(config))
1932 f.close()
1933 self.config.set('zuul', 'tenant_config',
1934 os.path.join(FIXTURE_DIR, f.name))
1935
1936 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001937 self.addCommitToRepo('common-config', 'add content from fixture',
1938 files, branch='master', tag='init')
1939
1940 return True
1941
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001942 def setupAllProjectKeys(self):
1943 if self.create_project_keys:
1944 return
1945
1946 path = self.config.get('zuul', 'tenant_config')
1947 with open(os.path.join(FIXTURE_DIR, path)) as f:
1948 tenant_config = yaml.safe_load(f.read())
1949 for tenant in tenant_config:
1950 sources = tenant['tenant']['source']
1951 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001952 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001953 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001954 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001955 self.setupProjectKeys(source, project)
1956
1957 def setupProjectKeys(self, source, project):
1958 # Make sure we set up an RSA key for the project so that we
1959 # don't spend time generating one:
1960
1961 key_root = os.path.join(self.state_root, 'keys')
1962 if not os.path.isdir(key_root):
1963 os.mkdir(key_root, 0o700)
1964 private_key_file = os.path.join(key_root, source, project + '.pem')
1965 private_key_dir = os.path.dirname(private_key_file)
1966 self.log.debug("Installing test keys for project %s at %s" % (
1967 project, private_key_file))
1968 if not os.path.isdir(private_key_dir):
1969 os.makedirs(private_key_dir)
1970 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1971 with open(private_key_file, 'w') as o:
1972 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001973
James E. Blair498059b2016-12-20 13:50:13 -08001974 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001975 self.zk_chroot_fixture = self.useFixture(
1976 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001977 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001978 self.zk_chroot_fixture.zookeeper_host,
1979 self.zk_chroot_fixture.zookeeper_port,
1980 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001981
James E. Blair96c6bf82016-01-15 16:20:40 -08001982 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001983 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001984
1985 files = {}
1986 for (dirpath, dirnames, filenames) in os.walk(source_path):
1987 for filename in filenames:
1988 test_tree_filepath = os.path.join(dirpath, filename)
1989 common_path = os.path.commonprefix([test_tree_filepath,
1990 source_path])
1991 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1992 with open(test_tree_filepath, 'r') as f:
1993 content = f.read()
1994 files[relative_filepath] = content
1995 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001996 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001997
James E. Blaire18d4602017-01-05 11:17:28 -08001998 def assertNodepoolState(self):
1999 # Make sure that there are no pending requests
2000
2001 requests = self.fake_nodepool.getNodeRequests()
2002 self.assertEqual(len(requests), 0)
2003
2004 nodes = self.fake_nodepool.getNodes()
2005 for node in nodes:
2006 self.assertFalse(node['_lock'], "Node %s is locked" %
2007 (node['_oid'],))
2008
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002009 def assertNoGeneratedKeys(self):
2010 # Make sure that Zuul did not generate any project keys
2011 # (unless it was supposed to).
2012
2013 if self.create_project_keys:
2014 return
2015
2016 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2017 test_key = i.read()
2018
2019 key_root = os.path.join(self.state_root, 'keys')
2020 for root, dirname, files in os.walk(key_root):
2021 for fn in files:
2022 with open(os.path.join(root, fn)) as f:
2023 self.assertEqual(test_key, f.read())
2024
Clark Boylanb640e052014-04-03 16:41:46 -07002025 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002026 self.log.debug("Assert final state")
2027 # Make sure no jobs are running
2028 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002029 # Make sure that git.Repo objects have been garbage collected.
2030 repos = []
2031 gc.collect()
2032 for obj in gc.get_objects():
2033 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002034 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002035 repos.append(obj)
2036 self.assertEqual(len(repos), 0)
2037 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002038 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002039 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002040 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002041 for tenant in self.sched.abide.tenants.values():
2042 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002043 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002044 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002045
2046 def shutdown(self):
2047 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002048 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002049 self.merge_server.stop()
2050 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002051 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002052 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002053 self.sched.stop()
2054 self.sched.join()
2055 self.statsd.stop()
2056 self.statsd.join()
2057 self.webapp.stop()
2058 self.webapp.join()
2059 self.rpc.stop()
2060 self.rpc.join()
2061 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002062 self.fake_nodepool.stop()
2063 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002064 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002065 # we whitelist watchdog threads as they have relatively long delays
2066 # before noticing they should exit, but they should exit on their own.
2067 threads = [t for t in threading.enumerate()
2068 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002069 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002070 log_str = ""
2071 for thread_id, stack_frame in sys._current_frames().items():
2072 log_str += "Thread: %s\n" % thread_id
2073 log_str += "".join(traceback.format_stack(stack_frame))
2074 self.log.debug(log_str)
2075 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002076
James E. Blaira002b032017-04-18 10:35:48 -07002077 def assertCleanShutdown(self):
2078 pass
2079
James E. Blairc4ba97a2017-04-19 16:26:24 -07002080 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002081 parts = project.split('/')
2082 path = os.path.join(self.upstream_root, *parts[:-1])
2083 if not os.path.exists(path):
2084 os.makedirs(path)
2085 path = os.path.join(self.upstream_root, project)
2086 repo = git.Repo.init(path)
2087
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002088 with repo.config_writer() as config_writer:
2089 config_writer.set_value('user', 'email', 'user@example.com')
2090 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002091
Clark Boylanb640e052014-04-03 16:41:46 -07002092 repo.index.commit('initial commit')
2093 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002094 if tag:
2095 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002096
James E. Blair97d902e2014-08-21 13:25:56 -07002097 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002098 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002099 repo.git.clean('-x', '-f', '-d')
2100
James E. Blair97d902e2014-08-21 13:25:56 -07002101 def create_branch(self, project, branch):
2102 path = os.path.join(self.upstream_root, project)
2103 repo = git.Repo.init(path)
2104 fn = os.path.join(path, 'README')
2105
2106 branch_head = repo.create_head(branch)
2107 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002108 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002109 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002110 f.close()
2111 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002112 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002113
James E. Blair97d902e2014-08-21 13:25:56 -07002114 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002115 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002116 repo.git.clean('-x', '-f', '-d')
2117
Sachi King9f16d522016-03-16 12:20:45 +11002118 def create_commit(self, project):
2119 path = os.path.join(self.upstream_root, project)
2120 repo = git.Repo(path)
2121 repo.head.reference = repo.heads['master']
2122 file_name = os.path.join(path, 'README')
2123 with open(file_name, 'a') as f:
2124 f.write('creating fake commit\n')
2125 repo.index.add([file_name])
2126 commit = repo.index.commit('Creating a fake commit')
2127 return commit.hexsha
2128
James E. Blairf4a5f022017-04-18 14:01:10 -07002129 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002130 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002131 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002132 while len(self.builds):
2133 self.release(self.builds[0])
2134 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002135 i += 1
2136 if count is not None and i >= count:
2137 break
James E. Blairb8c16472015-05-05 14:55:26 -07002138
Clark Boylanb640e052014-04-03 16:41:46 -07002139 def release(self, job):
2140 if isinstance(job, FakeBuild):
2141 job.release()
2142 else:
2143 job.waiting = False
2144 self.log.debug("Queued job %s released" % job.unique)
2145 self.gearman_server.wakeConnections()
2146
2147 def getParameter(self, job, name):
2148 if isinstance(job, FakeBuild):
2149 return job.parameters[name]
2150 else:
2151 parameters = json.loads(job.arguments)
2152 return parameters[name]
2153
Clark Boylanb640e052014-04-03 16:41:46 -07002154 def haveAllBuildsReported(self):
2155 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002156 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002157 return False
2158 # Find out if every build that the worker has completed has been
2159 # reported back to Zuul. If it hasn't then that means a Gearman
2160 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002161 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002162 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002163 if not zbuild:
2164 # It has already been reported
2165 continue
2166 # It hasn't been reported yet.
2167 return False
2168 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002169 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002170 if connection.state == 'GRAB_WAIT':
2171 return False
2172 return True
2173
2174 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002175 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002176 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002177 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002178 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002179 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002180 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002181 for j in conn.related_jobs.values():
2182 if j.unique == build.uuid:
2183 client_job = j
2184 break
2185 if not client_job:
2186 self.log.debug("%s is not known to the gearman client" %
2187 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002188 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002189 if not client_job.handle:
2190 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002191 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002192 server_job = self.gearman_server.jobs.get(client_job.handle)
2193 if not server_job:
2194 self.log.debug("%s is not known to the gearman server" %
2195 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002196 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002197 if not hasattr(server_job, 'waiting'):
2198 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002199 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002200 if server_job.waiting:
2201 continue
James E. Blair17302972016-08-10 16:11:42 -07002202 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002203 self.log.debug("%s has not reported start" % build)
2204 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002205 worker_build = self.executor_server.job_builds.get(
2206 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002207 if worker_build:
2208 if worker_build.isWaiting():
2209 continue
2210 else:
2211 self.log.debug("%s is running" % worker_build)
2212 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002213 else:
James E. Blair962220f2016-08-03 11:22:38 -07002214 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002215 return False
James E. Blaira002b032017-04-18 10:35:48 -07002216 for (build_uuid, job_worker) in \
2217 self.executor_server.job_workers.items():
2218 if build_uuid not in seen_builds:
2219 self.log.debug("%s is not finalized" % build_uuid)
2220 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002221 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002222
James E. Blairdce6cea2016-12-20 16:45:32 -08002223 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002224 if self.fake_nodepool.paused:
2225 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002226 if self.sched.nodepool.requests:
2227 return False
2228 return True
2229
Jan Hruban6b71aff2015-10-22 16:58:08 +02002230 def eventQueuesEmpty(self):
2231 for queue in self.event_queues:
2232 yield queue.empty()
2233
2234 def eventQueuesJoin(self):
2235 for queue in self.event_queues:
2236 queue.join()
2237
Clark Boylanb640e052014-04-03 16:41:46 -07002238 def waitUntilSettled(self):
2239 self.log.debug("Waiting until settled...")
2240 start = time.time()
2241 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002242 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002243 self.log.error("Timeout waiting for Zuul to settle")
2244 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002245 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002246 self.log.error(" %s: %s" % (queue, queue.empty()))
2247 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002248 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002249 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002250 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002251 self.log.error("All requests completed: %s" %
2252 (self.areAllNodeRequestsComplete(),))
2253 self.log.error("Merge client jobs: %s" %
2254 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002255 raise Exception("Timeout waiting for Zuul to settle")
2256 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002257
Paul Belanger174a8272017-03-14 13:20:10 -04002258 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002259 # have all build states propogated to zuul?
2260 if self.haveAllBuildsReported():
2261 # Join ensures that the queue is empty _and_ events have been
2262 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002263 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002264 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002265 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002266 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002267 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002268 self.areAllNodeRequestsComplete() and
2269 all(self.eventQueuesEmpty())):
2270 # The queue empty check is placed at the end to
2271 # ensure that if a component adds an event between
2272 # when locked the run handler and checked that the
2273 # components were stable, we don't erroneously
2274 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002275 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002276 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002277 self.log.debug("...settled.")
2278 return
2279 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002280 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002281 self.sched.wake_event.wait(0.1)
2282
2283 def countJobResults(self, jobs, result):
2284 jobs = filter(lambda x: x.result == result, jobs)
2285 return len(jobs)
2286
James E. Blair96c6bf82016-01-15 16:20:40 -08002287 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002288 for job in self.history:
2289 if (job.name == name and
2290 (project is None or
2291 job.parameters['ZUUL_PROJECT'] == project)):
2292 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002293 raise Exception("Unable to find job %s in history" % name)
2294
2295 def assertEmptyQueues(self):
2296 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002297 for tenant in self.sched.abide.tenants.values():
2298 for pipeline in tenant.layout.pipelines.values():
2299 for queue in pipeline.queues:
2300 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002301 print('pipeline %s queue %s contents %s' % (
2302 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002303 self.assertEqual(len(queue.queue), 0,
2304 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002305
2306 def assertReportedStat(self, key, value=None, kind=None):
2307 start = time.time()
2308 while time.time() < (start + 5):
2309 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002310 k, v = stat.split(':')
2311 if key == k:
2312 if value is None and kind is None:
2313 return
2314 elif value:
2315 if value == v:
2316 return
2317 elif kind:
2318 if v.endswith('|' + kind):
2319 return
2320 time.sleep(0.1)
2321
Clark Boylanb640e052014-04-03 16:41:46 -07002322 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002323
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002324 def assertBuilds(self, builds):
2325 """Assert that the running builds are as described.
2326
2327 The list of running builds is examined and must match exactly
2328 the list of builds described by the input.
2329
2330 :arg list builds: A list of dictionaries. Each item in the
2331 list must match the corresponding build in the build
2332 history, and each element of the dictionary must match the
2333 corresponding attribute of the build.
2334
2335 """
James E. Blair3158e282016-08-19 09:34:11 -07002336 try:
2337 self.assertEqual(len(self.builds), len(builds))
2338 for i, d in enumerate(builds):
2339 for k, v in d.items():
2340 self.assertEqual(
2341 getattr(self.builds[i], k), v,
2342 "Element %i in builds does not match" % (i,))
2343 except Exception:
2344 for build in self.builds:
2345 self.log.error("Running build: %s" % build)
2346 else:
2347 self.log.error("No running builds")
2348 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002349
James E. Blairb536ecc2016-08-31 10:11:42 -07002350 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002351 """Assert that the completed builds are as described.
2352
2353 The list of completed builds is examined and must match
2354 exactly the list of builds described by the input.
2355
2356 :arg list history: A list of dictionaries. Each item in the
2357 list must match the corresponding build in the build
2358 history, and each element of the dictionary must match the
2359 corresponding attribute of the build.
2360
James E. Blairb536ecc2016-08-31 10:11:42 -07002361 :arg bool ordered: If true, the history must match the order
2362 supplied, if false, the builds are permitted to have
2363 arrived in any order.
2364
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002365 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002366 def matches(history_item, item):
2367 for k, v in item.items():
2368 if getattr(history_item, k) != v:
2369 return False
2370 return True
James E. Blair3158e282016-08-19 09:34:11 -07002371 try:
2372 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002373 if ordered:
2374 for i, d in enumerate(history):
2375 if not matches(self.history[i], d):
2376 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002377 "Element %i in history does not match %s" %
2378 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002379 else:
2380 unseen = self.history[:]
2381 for i, d in enumerate(history):
2382 found = False
2383 for unseen_item in unseen:
2384 if matches(unseen_item, d):
2385 found = True
2386 unseen.remove(unseen_item)
2387 break
2388 if not found:
2389 raise Exception("No match found for element %i "
2390 "in history" % (i,))
2391 if unseen:
2392 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002393 except Exception:
2394 for build in self.history:
2395 self.log.error("Completed build: %s" % build)
2396 else:
2397 self.log.error("No completed builds")
2398 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002399
James E. Blair6ac368c2016-12-22 18:07:20 -08002400 def printHistory(self):
2401 """Log the build history.
2402
2403 This can be useful during tests to summarize what jobs have
2404 completed.
2405
2406 """
2407 self.log.debug("Build history:")
2408 for build in self.history:
2409 self.log.debug(build)
2410
James E. Blair59fdbac2015-12-07 17:08:06 -08002411 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002412 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2413
James E. Blair9ea70072017-04-19 16:05:30 -07002414 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002415 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002416 if not os.path.exists(root):
2417 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002418 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2419 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002420- tenant:
2421 name: openstack
2422 source:
2423 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002424 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002425 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002426 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002427 - org/project
2428 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002429 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002430 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002431 self.config.set('zuul', 'tenant_config',
2432 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002433 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002434
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002435 def addCommitToRepo(self, project, message, files,
2436 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002437 path = os.path.join(self.upstream_root, project)
2438 repo = git.Repo(path)
2439 repo.head.reference = branch
2440 zuul.merger.merger.reset_repo_to_head(repo)
2441 for fn, content in files.items():
2442 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002443 try:
2444 os.makedirs(os.path.dirname(fn))
2445 except OSError:
2446 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002447 with open(fn, 'w') as f:
2448 f.write(content)
2449 repo.index.add([fn])
2450 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002451 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002452 repo.heads[branch].commit = commit
2453 repo.head.reference = branch
2454 repo.git.clean('-x', '-f', '-d')
2455 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002456 if tag:
2457 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002458 return before
2459
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002460 def commitConfigUpdate(self, project_name, source_name):
2461 """Commit an update to zuul.yaml
2462
2463 This overwrites the zuul.yaml in the specificed project with
2464 the contents specified.
2465
2466 :arg str project_name: The name of the project containing
2467 zuul.yaml (e.g., common-config)
2468
2469 :arg str source_name: The path to the file (underneath the
2470 test fixture directory) whose contents should be used to
2471 replace zuul.yaml.
2472 """
2473
2474 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002475 files = {}
2476 with open(source_path, 'r') as f:
2477 data = f.read()
2478 layout = yaml.safe_load(data)
2479 files['zuul.yaml'] = data
2480 for item in layout:
2481 if 'job' in item:
2482 jobname = item['job']['name']
2483 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002484 before = self.addCommitToRepo(
2485 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002486 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002487 return before
2488
James E. Blair7fc8daa2016-08-08 15:37:15 -07002489 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002490
James E. Blair7fc8daa2016-08-08 15:37:15 -07002491 """Inject a Fake (Gerrit) event.
2492
2493 This method accepts a JSON-encoded event and simulates Zuul
2494 having received it from Gerrit. It could (and should)
2495 eventually apply to any connection type, but is currently only
2496 used with Gerrit connections. The name of the connection is
2497 used to look up the corresponding server, and the event is
2498 simulated as having been received by all Zuul connections
2499 attached to that server. So if two Gerrit connections in Zuul
2500 are connected to the same Gerrit server, and you invoke this
2501 method specifying the name of one of them, the event will be
2502 received by both.
2503
2504 .. note::
2505
2506 "self.fake_gerrit.addEvent" calls should be migrated to
2507 this method.
2508
2509 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002510 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002511 :arg str event: The JSON-encoded event.
2512
2513 """
2514 specified_conn = self.connections.connections[connection]
2515 for conn in self.connections.connections.values():
2516 if (isinstance(conn, specified_conn.__class__) and
2517 specified_conn.server == conn.server):
2518 conn.addEvent(event)
2519
James E. Blair3f876d52016-07-22 13:07:14 -07002520
2521class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002522 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002523 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002524
Joshua Heskethd78b4482015-09-14 16:56:34 -06002525
2526class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002527 def setup_config(self):
2528 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002529 for section_name in self.config.sections():
2530 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2531 section_name, re.I)
2532 if not con_match:
2533 continue
2534
2535 if self.config.get(section_name, 'driver') == 'sql':
2536 f = MySQLSchemaFixture()
2537 self.useFixture(f)
2538 if (self.config.get(section_name, 'dburi') ==
2539 '$MYSQL_FIXTURE_DBURI$'):
2540 self.config.set(section_name, 'dburi', f.dburi)