blob: 937d60f18fcdedbd8730669fb69c238f88a9f0d6 [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
Jan Hruban16ad31f2015-11-07 14:39:07 +0100620 def addLabel(self, name):
621 if name not in self.labels:
622 self.labels.append(name)
623 self._updateTimeStamp()
624 return self._getLabelEvent(name)
625
626 def removeLabel(self, name):
627 if name in self.labels:
628 self.labels.remove(name)
629 self._updateTimeStamp()
630 return self._getUnlabelEvent(name)
631
632 def _getLabelEvent(self, label):
633 name = 'pull_request'
634 data = {
635 'action': 'labeled',
636 'pull_request': {
637 'number': self.number,
638 'updated_at': self.updated_at,
639 'base': {
640 'ref': self.branch,
641 'repo': {
642 'full_name': self.project
643 }
644 },
645 'head': {
646 'sha': self.head_sha
647 }
648 },
649 'label': {
650 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100651 },
652 'sender': {
653 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100654 }
655 }
656 return (name, data)
657
658 def _getUnlabelEvent(self, label):
659 name = 'pull_request'
660 data = {
661 'action': 'unlabeled',
662 'pull_request': {
663 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100664 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100665 'updated_at': self.updated_at,
666 'base': {
667 'ref': self.branch,
668 'repo': {
669 'full_name': self.project
670 }
671 },
672 'head': {
673 'sha': self.head_sha
674 }
675 },
676 'label': {
677 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100678 },
679 'sender': {
680 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100681 }
682 }
683 return (name, data)
684
Gregory Haynes4fc12542015-04-22 20:38:06 -0700685 def _getRepo(self):
686 repo_path = os.path.join(self.upstream_root, self.project)
687 return git.Repo(repo_path)
688
689 def _createPRRef(self):
690 repo = self._getRepo()
691 GithubChangeReference.create(
692 repo, self._getPRReference(), 'refs/tags/init')
693
Jan Hruban570d01c2016-03-10 21:51:32 +0100694 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700695 repo = self._getRepo()
696 ref = repo.references[self._getPRReference()]
697 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100698 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700699 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100700 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700701 repo.head.reference = ref
702 zuul.merger.merger.reset_repo_to_head(repo)
703 repo.git.clean('-x', '-f', '-d')
704
Jan Hruban570d01c2016-03-10 21:51:32 +0100705 if files:
706 fn = files[0]
707 self.files = files
708 else:
709 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
710 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100711 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700712 fn = os.path.join(repo.working_dir, fn)
713 f = open(fn, 'w')
714 with open(fn, 'w') as f:
715 f.write("test %s %s\n" %
716 (self.branch, self.number))
717 repo.index.add([fn])
718
719 self.head_sha = repo.index.commit(msg).hexsha
720 repo.head.reference = 'master'
721 zuul.merger.merger.reset_repo_to_head(repo)
722 repo.git.clean('-x', '-f', '-d')
723 repo.heads['master'].checkout()
724
725 def _updateTimeStamp(self):
726 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
727
728 def getPRHeadSha(self):
729 repo = self._getRepo()
730 return repo.references[self._getPRReference()].commit.hexsha
731
Jan Hrubane252a732017-01-03 15:03:09 +0100732 def setStatus(self, state, url, description, context):
733 self.statuses[context] = {
734 'state': state,
735 'url': url,
736 'description': description
737 }
738
739 def _clearStatuses(self):
740 self.statuses = {}
741
Gregory Haynes4fc12542015-04-22 20:38:06 -0700742 def _getPRReference(self):
743 return '%s/head' % self.number
744
745 def _getPullRequestEvent(self, action):
746 name = 'pull_request'
747 data = {
748 'action': action,
749 'number': self.number,
750 'pull_request': {
751 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100752 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700753 'updated_at': self.updated_at,
754 'base': {
755 'ref': self.branch,
756 'repo': {
757 'full_name': self.project
758 }
759 },
760 'head': {
761 'sha': self.head_sha
762 }
Jan Hruban3b415922016-02-03 13:10:22 +0100763 },
764 'sender': {
765 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700766 }
767 }
768 return (name, data)
769
770
771class FakeGithubConnection(githubconnection.GithubConnection):
772 log = logging.getLogger("zuul.test.FakeGithubConnection")
773
774 def __init__(self, driver, connection_name, connection_config,
775 upstream_root=None):
776 super(FakeGithubConnection, self).__init__(driver, connection_name,
777 connection_config)
778 self.connection_name = connection_name
779 self.pr_number = 0
780 self.pull_requests = []
781 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100782 self.merge_failure = False
783 self.merge_not_allowed_count = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700784
Jan Hruban570d01c2016-03-10 21:51:32 +0100785 def openFakePullRequest(self, project, branch, subject, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700786 self.pr_number += 1
787 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100788 self, self.pr_number, project, branch, subject, self.upstream_root,
789 files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700790 self.pull_requests.append(pull_request)
791 return pull_request
792
Wayne1a78c612015-06-11 17:14:13 -0700793 def getPushEvent(self, project, ref, old_rev=None, new_rev=None):
794 if not old_rev:
795 old_rev = '00000000000000000000000000000000'
796 if not new_rev:
797 new_rev = random_sha1()
798 name = 'push'
799 data = {
800 'ref': ref,
801 'before': old_rev,
802 'after': new_rev,
803 'repository': {
804 'full_name': project
805 }
806 }
807 return (name, data)
808
Gregory Haynes4fc12542015-04-22 20:38:06 -0700809 def emitEvent(self, event):
810 """Emulates sending the GitHub webhook event to the connection."""
811 port = self.webapp.server.socket.getsockname()[1]
812 name, data = event
813 payload = json.dumps(data)
814 headers = {'X-Github-Event': name}
815 req = urllib.request.Request(
816 'http://localhost:%s/connection/%s/payload'
817 % (port, self.connection_name),
818 data=payload, headers=headers)
819 urllib.request.urlopen(req)
820
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200821 def getPull(self, project, number):
822 pr = self.pull_requests[number - 1]
823 data = {
824 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +0100825 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200826 'updated_at': pr.updated_at,
827 'base': {
828 'repo': {
829 'full_name': pr.project
830 },
831 'ref': pr.branch,
832 },
Jan Hruban37615e52015-11-19 14:30:49 +0100833 'mergeable': True,
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200834 'head': {
835 'sha': pr.head_sha
836 }
837 }
838 return data
839
Jan Hruban570d01c2016-03-10 21:51:32 +0100840 def getPullFileNames(self, project, number):
841 pr = self.pull_requests[number - 1]
842 return pr.files
843
Jan Hruban3b415922016-02-03 13:10:22 +0100844 def getUser(self, login):
845 data = {
846 'username': login,
847 'name': 'Github User',
848 'email': 'github.user@example.com'
849 }
850 return data
851
Gregory Haynes4fc12542015-04-22 20:38:06 -0700852 def getGitUrl(self, project):
853 return os.path.join(self.upstream_root, str(project))
854
Jan Hruban6d53c5e2015-10-24 03:03:34 +0200855 def real_getGitUrl(self, project):
856 return super(FakeGithubConnection, self).getGitUrl(project)
857
Gregory Haynes4fc12542015-04-22 20:38:06 -0700858 def getProjectBranches(self, project):
859 """Masks getProjectBranches since we don't have a real github"""
860
861 # just returns master for now
862 return ['master']
863
Jan Hrubane252a732017-01-03 15:03:09 +0100864 def commentPull(self, project, pr_number, message):
Wayne40f40042015-06-12 16:56:30 -0700865 pull_request = self.pull_requests[pr_number - 1]
866 pull_request.addComment(message)
867
Jan Hruban3b415922016-02-03 13:10:22 +0100868 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jan Hruban49bff072015-11-03 11:45:46 +0100869 pull_request = self.pull_requests[pr_number - 1]
870 if self.merge_failure:
871 raise Exception('Pull request was not merged')
872 if self.merge_not_allowed_count > 0:
873 self.merge_not_allowed_count -= 1
874 raise MergeFailure('Merge was not successful due to mergeability'
875 ' conflict')
876 pull_request.is_merged = True
Jan Hruban3b415922016-02-03 13:10:22 +0100877 pull_request.merge_message = commit_message
Jan Hruban49bff072015-11-03 11:45:46 +0100878
Jan Hrubane252a732017-01-03 15:03:09 +0100879 def setCommitStatus(self, project, sha, state,
880 url='', description='', context=''):
881 owner, proj = project.split('/')
882 for pr in self.pull_requests:
883 pr_owner, pr_project = pr.project.split('/')
884 if (pr_owner == owner and pr_project == proj and
885 pr.head_sha == sha):
886 pr.setStatus(state, url, description, context)
887
Jan Hruban16ad31f2015-11-07 14:39:07 +0100888 def labelPull(self, project, pr_number, label):
889 pull_request = self.pull_requests[pr_number - 1]
890 pull_request.addLabel(label)
891
892 def unlabelPull(self, project, pr_number, label):
893 pull_request = self.pull_requests[pr_number - 1]
894 pull_request.removeLabel(label)
895
Gregory Haynes4fc12542015-04-22 20:38:06 -0700896
Clark Boylanb640e052014-04-03 16:41:46 -0700897class BuildHistory(object):
898 def __init__(self, **kw):
899 self.__dict__.update(kw)
900
901 def __repr__(self):
James E. Blair17302972016-08-10 16:11:42 -0700902 return ("<Completed build, result: %s name: %s uuid: %s changes: %s>" %
903 (self.result, self.name, self.uuid, self.changes))
Clark Boylanb640e052014-04-03 16:41:46 -0700904
905
906class FakeURLOpener(object):
Jan Hruban6b71aff2015-10-22 16:58:08 +0200907 def __init__(self, upstream_root, url):
Clark Boylanb640e052014-04-03 16:41:46 -0700908 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700909 self.url = url
910
911 def read(self):
Morgan Fainberg293f7f82016-05-30 14:01:22 -0700912 res = urllib.parse.urlparse(self.url)
Clark Boylanb640e052014-04-03 16:41:46 -0700913 path = res.path
914 project = '/'.join(path.split('/')[2:-2])
915 ret = '001e# service=git-upload-pack\n'
916 ret += ('000000a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
917 'multi_ack thin-pack side-band side-band-64k ofs-delta '
918 'shallow no-progress include-tag multi_ack_detailed no-done\n')
919 path = os.path.join(self.upstream_root, project)
920 repo = git.Repo(path)
921 for ref in repo.refs:
922 r = ref.object.hexsha + ' ' + ref.path + '\n'
923 ret += '%04x%s' % (len(r) + 4, r)
924 ret += '0000'
925 return ret
926
927
Clark Boylanb640e052014-04-03 16:41:46 -0700928class FakeStatsd(threading.Thread):
929 def __init__(self):
930 threading.Thread.__init__(self)
931 self.daemon = True
932 self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
933 self.sock.bind(('', 0))
934 self.port = self.sock.getsockname()[1]
935 self.wake_read, self.wake_write = os.pipe()
936 self.stats = []
937
938 def run(self):
939 while True:
940 poll = select.poll()
941 poll.register(self.sock, select.POLLIN)
942 poll.register(self.wake_read, select.POLLIN)
943 ret = poll.poll()
944 for (fd, event) in ret:
945 if fd == self.sock.fileno():
946 data = self.sock.recvfrom(1024)
947 if not data:
948 return
949 self.stats.append(data[0])
950 if fd == self.wake_read:
951 return
952
953 def stop(self):
954 os.write(self.wake_write, '1\n')
955
956
James E. Blaire1767bc2016-08-02 10:00:27 -0700957class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -0700958 log = logging.getLogger("zuul.test")
959
Paul Belanger174a8272017-03-14 13:20:10 -0400960 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -0700961 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -0400962 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -0700963 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -0700964 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -0700965 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -0700966 self.parameters = json.loads(job.arguments)
James E. Blair34776ee2016-08-25 13:53:54 -0700967 # TODOv3(jeblair): self.node is really "the image of the node
968 # assigned". We should rename it (self.node_image?) if we
969 # keep using it like this, or we may end up exposing more of
970 # the complexity around multi-node jobs here
971 # (self.nodes[0].image?)
972 self.node = None
973 if len(self.parameters.get('nodes')) == 1:
974 self.node = self.parameters['nodes'][0]['image']
Clark Boylanb640e052014-04-03 16:41:46 -0700975 self.unique = self.parameters['ZUUL_UUID']
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100976 self.pipeline = self.parameters['ZUUL_PIPELINE']
977 self.project = self.parameters['ZUUL_PROJECT']
James E. Blair3f876d52016-07-22 13:07:14 -0700978 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -0700979 self.wait_condition = threading.Condition()
980 self.waiting = False
981 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -0500982 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -0700983 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -0700984 self.changes = None
985 if 'ZUUL_CHANGE_IDS' in self.parameters:
986 self.changes = self.parameters['ZUUL_CHANGE_IDS']
Clark Boylanb640e052014-04-03 16:41:46 -0700987
James E. Blair3158e282016-08-19 09:34:11 -0700988 def __repr__(self):
989 waiting = ''
990 if self.waiting:
991 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +1100992 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
993 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -0700994
Clark Boylanb640e052014-04-03 16:41:46 -0700995 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -0700996 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -0700997 self.wait_condition.acquire()
998 self.wait_condition.notify()
999 self.waiting = False
1000 self.log.debug("Build %s released" % self.unique)
1001 self.wait_condition.release()
1002
1003 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001004 """Return whether this build is being held.
1005
1006 :returns: Whether the build is being held.
1007 :rtype: bool
1008 """
1009
Clark Boylanb640e052014-04-03 16:41:46 -07001010 self.wait_condition.acquire()
1011 if self.waiting:
1012 ret = True
1013 else:
1014 ret = False
1015 self.wait_condition.release()
1016 return ret
1017
1018 def _wait(self):
1019 self.wait_condition.acquire()
1020 self.waiting = True
1021 self.log.debug("Build %s waiting" % self.unique)
1022 self.wait_condition.wait()
1023 self.wait_condition.release()
1024
1025 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001026 self.log.debug('Running build %s' % self.unique)
1027
Paul Belanger174a8272017-03-14 13:20:10 -04001028 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001029 self.log.debug('Holding build %s' % self.unique)
1030 self._wait()
1031 self.log.debug("Build %s continuing" % self.unique)
1032
James E. Blair412fba82017-01-26 15:00:50 -08001033 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blaira5dba232016-08-08 15:53:24 -07001034 if (('ZUUL_REF' in self.parameters) and self.shouldFail()):
James E. Blair412fba82017-01-26 15:00:50 -08001035 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001036 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001037 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001038 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001039 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001040
James E. Blaire1767bc2016-08-02 10:00:27 -07001041 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001042
James E. Blaira5dba232016-08-08 15:53:24 -07001043 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001044 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001045 for change in changes:
1046 if self.hasChanges(change):
1047 return True
1048 return False
1049
James E. Blaire7b99a02016-08-05 14:27:34 -07001050 def hasChanges(self, *changes):
1051 """Return whether this build has certain changes in its git repos.
1052
1053 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001054 are expected to be present (in order) in the git repository of
1055 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001056
1057 :returns: Whether the build has the indicated changes.
1058 :rtype: bool
1059
1060 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001061 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001062 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001063 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001064 try:
1065 repo = git.Repo(path)
1066 except NoSuchPathError as e:
1067 self.log.debug('%s' % e)
1068 return False
1069 ref = self.parameters['ZUUL_REF']
1070 repo_messages = [c.message.strip() for c in repo.iter_commits(ref)]
1071 commit_message = '%s-1' % change.subject
1072 self.log.debug("Checking if build %s has changes; commit_message "
1073 "%s; repo_messages %s" % (self, commit_message,
1074 repo_messages))
1075 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001076 self.log.debug(" messages do not match")
1077 return False
1078 self.log.debug(" OK")
1079 return True
1080
Clark Boylanb640e052014-04-03 16:41:46 -07001081
Paul Belanger174a8272017-03-14 13:20:10 -04001082class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1083 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001084
Paul Belanger174a8272017-03-14 13:20:10 -04001085 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001086 they will report that they have started but then pause until
1087 released before reporting completion. This attribute may be
1088 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001089 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001090 be explicitly released.
1091
1092 """
James E. Blairf5dbd002015-12-23 15:26:17 -08001093 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001094 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001095 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001096 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001097 self.hold_jobs_in_build = False
1098 self.lock = threading.Lock()
1099 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001100 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001101 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001102 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001103
James E. Blaira5dba232016-08-08 15:53:24 -07001104 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001105 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001106
1107 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001108 :arg Change change: The :py:class:`~tests.base.FakeChange`
1109 instance which should cause the job to fail. This job
1110 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001111
1112 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001113 l = self.fail_tests.get(name, [])
1114 l.append(change)
1115 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001116
James E. Blair962220f2016-08-03 11:22:38 -07001117 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001118 """Release a held build.
1119
1120 :arg str regex: A regular expression which, if supplied, will
1121 cause only builds with matching names to be released. If
1122 not supplied, all builds will be released.
1123
1124 """
James E. Blair962220f2016-08-03 11:22:38 -07001125 builds = self.running_builds[:]
1126 self.log.debug("Releasing build %s (%s)" % (regex,
1127 len(self.running_builds)))
1128 for build in builds:
1129 if not regex or re.match(regex, build.name):
1130 self.log.debug("Releasing build %s" %
1131 (build.parameters['ZUUL_UUID']))
1132 build.release()
1133 else:
1134 self.log.debug("Not releasing build %s" %
1135 (build.parameters['ZUUL_UUID']))
1136 self.log.debug("Done releasing builds %s (%s)" %
1137 (regex, len(self.running_builds)))
1138
Paul Belanger174a8272017-03-14 13:20:10 -04001139 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001140 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001141 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001142 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001143 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001144 args = json.loads(job.arguments)
James E. Blair490cf042017-02-24 23:07:21 -05001145 args['vars']['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001146 job.arguments = json.dumps(args)
Joshua Hesketh50c21782016-10-13 21:34:14 +11001147 self.job_workers[job.unique] = RecordingAnsibleJob(self, job)
1148 self.job_workers[job.unique].run()
James E. Blair17302972016-08-10 16:11:42 -07001149
1150 def stopJob(self, job):
1151 self.log.debug("handle stop")
1152 parameters = json.loads(job.arguments)
1153 uuid = parameters['uuid']
1154 for build in self.running_builds:
1155 if build.unique == uuid:
1156 build.aborted = True
1157 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001158 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001159
James E. Blaira002b032017-04-18 10:35:48 -07001160 def stop(self):
1161 for build in self.running_builds:
1162 build.release()
1163 super(RecordingExecutorServer, self).stop()
1164
Joshua Hesketh50c21782016-10-13 21:34:14 +11001165
Paul Belanger174a8272017-03-14 13:20:10 -04001166class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001167 def doMergeChanges(self, items):
1168 # Get a merger in order to update the repos involved in this job.
1169 commit = super(RecordingAnsibleJob, self).doMergeChanges(items)
1170 if not commit: # merge conflict
1171 self.recordResult('MERGER_FAILURE')
1172 return commit
1173
1174 def recordResult(self, result):
Paul Belanger174a8272017-03-14 13:20:10 -04001175 build = self.executor_server.job_builds[self.job.unique]
Paul Belanger174a8272017-03-14 13:20:10 -04001176 self.executor_server.lock.acquire()
1177 self.executor_server.build_history.append(
James E. Blair17302972016-08-10 16:11:42 -07001178 BuildHistory(name=build.name, result=result, changes=build.changes,
1179 node=build.node, uuid=build.unique,
K Jonathan Harker2c1a6232017-02-21 14:34:08 -08001180 parameters=build.parameters, jobdir=build.jobdir,
James E. Blaire1767bc2016-08-02 10:00:27 -07001181 pipeline=build.parameters['ZUUL_PIPELINE'])
1182 )
Paul Belanger174a8272017-03-14 13:20:10 -04001183 self.executor_server.running_builds.remove(build)
1184 del self.executor_server.job_builds[self.job.unique]
1185 self.executor_server.lock.release()
K Jonathan Harkerae04e4c2017-03-15 19:07:11 -07001186
1187 def runPlaybooks(self, args):
1188 build = self.executor_server.job_builds[self.job.unique]
1189 build.jobdir = self.jobdir
1190
1191 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1192 self.recordResult(result)
James E. Blair412fba82017-01-26 15:00:50 -08001193 return result
1194
Monty Taylore6562aa2017-02-20 07:37:39 -05001195 def runAnsible(self, cmd, timeout, trusted=False):
Paul Belanger174a8272017-03-14 13:20:10 -04001196 build = self.executor_server.job_builds[self.job.unique]
James E. Blair412fba82017-01-26 15:00:50 -08001197
Paul Belanger174a8272017-03-14 13:20:10 -04001198 if self.executor_server._run_ansible:
Monty Taylorc231d932017-02-03 09:57:15 -06001199 result = super(RecordingAnsibleJob, self).runAnsible(
Monty Taylore6562aa2017-02-20 07:37:39 -05001200 cmd, timeout, trusted=trusted)
James E. Blair412fba82017-01-26 15:00:50 -08001201 else:
1202 result = build.run()
James E. Blaire1767bc2016-08-02 10:00:27 -07001203 return result
James E. Blairf5dbd002015-12-23 15:26:17 -08001204
James E. Blairad8dca02017-02-21 11:48:32 -05001205 def getHostList(self, args):
1206 self.log.debug("hostlist")
1207 hosts = super(RecordingAnsibleJob, self).getHostList(args)
Paul Belangerc5bf3752017-03-16 19:38:43 -04001208 for host in hosts:
1209 host['host_vars']['ansible_connection'] = 'local'
1210
1211 hosts.append(dict(
1212 name='localhost',
1213 host_vars=dict(ansible_connection='local'),
1214 host_keys=[]))
James E. Blairad8dca02017-02-21 11:48:32 -05001215 return hosts
1216
James E. Blairf5dbd002015-12-23 15:26:17 -08001217
Clark Boylanb640e052014-04-03 16:41:46 -07001218class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001219 """A Gearman server for use in tests.
1220
1221 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1222 added to the queue but will not be distributed to workers
1223 until released. This attribute may be changed at any time and
1224 will take effect for subsequently enqueued jobs, but
1225 previously held jobs will still need to be explicitly
1226 released.
1227
1228 """
1229
Clark Boylanb640e052014-04-03 16:41:46 -07001230 def __init__(self):
1231 self.hold_jobs_in_queue = False
1232 super(FakeGearmanServer, self).__init__(0)
1233
1234 def getJobForConnection(self, connection, peek=False):
1235 for queue in [self.high_queue, self.normal_queue, self.low_queue]:
1236 for job in queue:
1237 if not hasattr(job, 'waiting'):
Paul Belanger174a8272017-03-14 13:20:10 -04001238 if job.name.startswith('executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001239 job.waiting = self.hold_jobs_in_queue
1240 else:
1241 job.waiting = False
1242 if job.waiting:
1243 continue
1244 if job.name in connection.functions:
1245 if not peek:
1246 queue.remove(job)
1247 connection.related_jobs[job.handle] = job
1248 job.worker_connection = connection
1249 job.running = True
1250 return job
1251 return None
1252
1253 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001254 """Release a held job.
1255
1256 :arg str regex: A regular expression which, if supplied, will
1257 cause only jobs with matching names to be released. If
1258 not supplied, all jobs will be released.
1259 """
Clark Boylanb640e052014-04-03 16:41:46 -07001260 released = False
1261 qlen = (len(self.high_queue) + len(self.normal_queue) +
1262 len(self.low_queue))
1263 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1264 for job in self.getQueue():
Paul Belanger174a8272017-03-14 13:20:10 -04001265 if job.name != 'executor:execute':
Clark Boylanb640e052014-04-03 16:41:46 -07001266 continue
Paul Belanger6ab6af72016-11-06 11:32:59 -05001267 parameters = json.loads(job.arguments)
1268 if not regex or re.match(regex, parameters.get('job')):
Clark Boylanb640e052014-04-03 16:41:46 -07001269 self.log.debug("releasing queued job %s" %
1270 job.unique)
1271 job.waiting = False
1272 released = True
1273 else:
1274 self.log.debug("not releasing queued job %s" %
1275 job.unique)
1276 if released:
1277 self.wakeConnections()
1278 qlen = (len(self.high_queue) + len(self.normal_queue) +
1279 len(self.low_queue))
1280 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1281
1282
1283class FakeSMTP(object):
1284 log = logging.getLogger('zuul.FakeSMTP')
1285
1286 def __init__(self, messages, server, port):
1287 self.server = server
1288 self.port = port
1289 self.messages = messages
1290
1291 def sendmail(self, from_email, to_email, msg):
1292 self.log.info("Sending email from %s, to %s, with msg %s" % (
1293 from_email, to_email, msg))
1294
1295 headers = msg.split('\n\n', 1)[0]
1296 body = msg.split('\n\n', 1)[1]
1297
1298 self.messages.append(dict(
1299 from_email=from_email,
1300 to_email=to_email,
1301 msg=msg,
1302 headers=headers,
1303 body=body,
1304 ))
1305
1306 return True
1307
1308 def quit(self):
1309 return True
1310
1311
James E. Blairdce6cea2016-12-20 16:45:32 -08001312class FakeNodepool(object):
1313 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001314 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001315
1316 log = logging.getLogger("zuul.test.FakeNodepool")
1317
1318 def __init__(self, host, port, chroot):
1319 self.client = kazoo.client.KazooClient(
1320 hosts='%s:%s%s' % (host, port, chroot))
1321 self.client.start()
1322 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001323 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001324 self.thread = threading.Thread(target=self.run)
1325 self.thread.daemon = True
1326 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001327 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001328
1329 def stop(self):
1330 self._running = False
1331 self.thread.join()
1332 self.client.stop()
1333 self.client.close()
1334
1335 def run(self):
1336 while self._running:
1337 self._run()
1338 time.sleep(0.1)
1339
1340 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001341 if self.paused:
1342 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001343 for req in self.getNodeRequests():
1344 self.fulfillRequest(req)
1345
1346 def getNodeRequests(self):
1347 try:
1348 reqids = self.client.get_children(self.REQUEST_ROOT)
1349 except kazoo.exceptions.NoNodeError:
1350 return []
1351 reqs = []
1352 for oid in sorted(reqids):
1353 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001354 try:
1355 data, stat = self.client.get(path)
1356 data = json.loads(data)
1357 data['_oid'] = oid
1358 reqs.append(data)
1359 except kazoo.exceptions.NoNodeError:
1360 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001361 return reqs
1362
James E. Blaire18d4602017-01-05 11:17:28 -08001363 def getNodes(self):
1364 try:
1365 nodeids = self.client.get_children(self.NODE_ROOT)
1366 except kazoo.exceptions.NoNodeError:
1367 return []
1368 nodes = []
1369 for oid in sorted(nodeids):
1370 path = self.NODE_ROOT + '/' + oid
1371 data, stat = self.client.get(path)
1372 data = json.loads(data)
1373 data['_oid'] = oid
1374 try:
1375 lockfiles = self.client.get_children(path + '/lock')
1376 except kazoo.exceptions.NoNodeError:
1377 lockfiles = []
1378 if lockfiles:
1379 data['_lock'] = True
1380 else:
1381 data['_lock'] = False
1382 nodes.append(data)
1383 return nodes
1384
James E. Blaira38c28e2017-01-04 10:33:20 -08001385 def makeNode(self, request_id, node_type):
1386 now = time.time()
1387 path = '/nodepool/nodes/'
1388 data = dict(type=node_type,
1389 provider='test-provider',
1390 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001391 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001392 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001393 public_ipv4='127.0.0.1',
1394 private_ipv4=None,
1395 public_ipv6=None,
1396 allocated_to=request_id,
1397 state='ready',
1398 state_time=now,
1399 created_time=now,
1400 updated_time=now,
1401 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001402 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001403 executor='fake-nodepool')
James E. Blaira38c28e2017-01-04 10:33:20 -08001404 data = json.dumps(data)
1405 path = self.client.create(path, data,
1406 makepath=True,
1407 sequence=True)
1408 nodeid = path.split("/")[-1]
1409 return nodeid
1410
James E. Blair6ab79e02017-01-06 10:10:17 -08001411 def addFailRequest(self, request):
1412 self.fail_requests.add(request['_oid'])
1413
James E. Blairdce6cea2016-12-20 16:45:32 -08001414 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001415 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001416 return
1417 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001418 oid = request['_oid']
1419 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001420
James E. Blair6ab79e02017-01-06 10:10:17 -08001421 if oid in self.fail_requests:
1422 request['state'] = 'failed'
1423 else:
1424 request['state'] = 'fulfilled'
1425 nodes = []
1426 for node in request['node_types']:
1427 nodeid = self.makeNode(oid, node)
1428 nodes.append(nodeid)
1429 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001430
James E. Blaira38c28e2017-01-04 10:33:20 -08001431 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001432 path = self.REQUEST_ROOT + '/' + oid
1433 data = json.dumps(request)
1434 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
1435 self.client.set(path, data)
1436
1437
James E. Blair498059b2016-12-20 13:50:13 -08001438class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001439 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001440 super(ChrootedKazooFixture, self).__init__()
1441
1442 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1443 if ':' in zk_host:
1444 host, port = zk_host.split(':')
1445 else:
1446 host = zk_host
1447 port = None
1448
1449 self.zookeeper_host = host
1450
1451 if not port:
1452 self.zookeeper_port = 2181
1453 else:
1454 self.zookeeper_port = int(port)
1455
Clark Boylan621ec9a2017-04-07 17:41:33 -07001456 self.test_id = test_id
1457
James E. Blair498059b2016-12-20 13:50:13 -08001458 def _setUp(self):
1459 # Make sure the test chroot paths do not conflict
1460 random_bits = ''.join(random.choice(string.ascii_lowercase +
1461 string.ascii_uppercase)
1462 for x in range(8))
1463
Clark Boylan621ec9a2017-04-07 17:41:33 -07001464 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001465 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1466
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001467 self.addCleanup(self._cleanup)
1468
James E. Blair498059b2016-12-20 13:50:13 -08001469 # Ensure the chroot path exists and clean up any pre-existing znodes.
1470 _tmp_client = kazoo.client.KazooClient(
1471 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1472 _tmp_client.start()
1473
1474 if _tmp_client.exists(self.zookeeper_chroot):
1475 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1476
1477 _tmp_client.ensure_path(self.zookeeper_chroot)
1478 _tmp_client.stop()
1479 _tmp_client.close()
1480
James E. Blair498059b2016-12-20 13:50:13 -08001481 def _cleanup(self):
1482 '''Remove the chroot path.'''
1483 # Need a non-chroot'ed client to remove the chroot path
1484 _tmp_client = kazoo.client.KazooClient(
1485 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1486 _tmp_client.start()
1487 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1488 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001489 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001490
1491
Joshua Heskethd78b4482015-09-14 16:56:34 -06001492class MySQLSchemaFixture(fixtures.Fixture):
1493 def setUp(self):
1494 super(MySQLSchemaFixture, self).setUp()
1495
1496 random_bits = ''.join(random.choice(string.ascii_lowercase +
1497 string.ascii_uppercase)
1498 for x in range(8))
1499 self.name = '%s_%s' % (random_bits, os.getpid())
1500 self.passwd = uuid.uuid4().hex
1501 db = pymysql.connect(host="localhost",
1502 user="openstack_citest",
1503 passwd="openstack_citest",
1504 db="openstack_citest")
1505 cur = db.cursor()
1506 cur.execute("create database %s" % self.name)
1507 cur.execute(
1508 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1509 (self.name, self.name, self.passwd))
1510 cur.execute("flush privileges")
1511
1512 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1513 self.passwd,
1514 self.name)
1515 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1516 self.addCleanup(self.cleanup)
1517
1518 def cleanup(self):
1519 db = pymysql.connect(host="localhost",
1520 user="openstack_citest",
1521 passwd="openstack_citest",
1522 db="openstack_citest")
1523 cur = db.cursor()
1524 cur.execute("drop database %s" % self.name)
1525 cur.execute("drop user '%s'@'localhost'" % self.name)
1526 cur.execute("flush privileges")
1527
1528
Maru Newby3fe5f852015-01-13 04:22:14 +00001529class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001530 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001531 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001532
James E. Blair1c236df2017-02-01 14:07:24 -08001533 def attachLogs(self, *args):
1534 def reader():
1535 self._log_stream.seek(0)
1536 while True:
1537 x = self._log_stream.read(4096)
1538 if not x:
1539 break
1540 yield x.encode('utf8')
1541 content = testtools.content.content_from_reader(
1542 reader,
1543 testtools.content_type.UTF8_TEXT,
1544 False)
1545 self.addDetail('logging', content)
1546
Clark Boylanb640e052014-04-03 16:41:46 -07001547 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001548 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001549 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1550 try:
1551 test_timeout = int(test_timeout)
1552 except ValueError:
1553 # If timeout value is invalid do not set a timeout.
1554 test_timeout = 0
1555 if test_timeout > 0:
1556 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1557
1558 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1559 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1560 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1561 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1562 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1563 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1564 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1565 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1566 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1567 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001568 self._log_stream = StringIO()
1569 self.addOnException(self.attachLogs)
1570 else:
1571 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001572
James E. Blair1c236df2017-02-01 14:07:24 -08001573 handler = logging.StreamHandler(self._log_stream)
1574 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1575 '%(levelname)-8s %(message)s')
1576 handler.setFormatter(formatter)
1577
1578 logger = logging.getLogger()
1579 logger.setLevel(logging.DEBUG)
1580 logger.addHandler(handler)
1581
Clark Boylan3410d532017-04-25 12:35:29 -07001582 # Make sure we don't carry old handlers around in process state
1583 # which slows down test runs
1584 self.addCleanup(logger.removeHandler, handler)
1585 self.addCleanup(handler.close)
1586 self.addCleanup(handler.flush)
1587
James E. Blair1c236df2017-02-01 14:07:24 -08001588 # NOTE(notmorgan): Extract logging overrides for specific
1589 # libraries from the OS_LOG_DEFAULTS env and create loggers
1590 # for each. This is used to limit the output during test runs
1591 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001592 log_defaults_from_env = os.environ.get(
1593 'OS_LOG_DEFAULTS',
James E. Blair1c236df2017-02-01 14:07:24 -08001594 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001595
James E. Blairdce6cea2016-12-20 16:45:32 -08001596 if log_defaults_from_env:
1597 for default in log_defaults_from_env.split(','):
1598 try:
1599 name, level_str = default.split('=', 1)
1600 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001601 logger = logging.getLogger(name)
1602 logger.setLevel(level)
1603 logger.addHandler(handler)
1604 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001605 except ValueError:
1606 # NOTE(notmorgan): Invalid format of the log default,
1607 # skip and don't try and apply a logger for the
1608 # specified module
1609 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001610
Maru Newby3fe5f852015-01-13 04:22:14 +00001611
1612class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001613 """A test case with a functioning Zuul.
1614
1615 The following class variables are used during test setup and can
1616 be overidden by subclasses but are effectively read-only once a
1617 test method starts running:
1618
1619 :cvar str config_file: This points to the main zuul config file
1620 within the fixtures directory. Subclasses may override this
1621 to obtain a different behavior.
1622
1623 :cvar str tenant_config_file: This is the tenant config file
1624 (which specifies from what git repos the configuration should
1625 be loaded). It defaults to the value specified in
1626 `config_file` but can be overidden by subclasses to obtain a
1627 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001628 configuration. See also the :py:func:`simple_layout`
1629 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001630
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001631 :cvar bool create_project_keys: Indicates whether Zuul should
1632 auto-generate keys for each project, or whether the test
1633 infrastructure should insert dummy keys to save time during
1634 startup. Defaults to False.
1635
James E. Blaire7b99a02016-08-05 14:27:34 -07001636 The following are instance variables that are useful within test
1637 methods:
1638
1639 :ivar FakeGerritConnection fake_<connection>:
1640 A :py:class:`~tests.base.FakeGerritConnection` will be
1641 instantiated for each connection present in the config file
1642 and stored here. For instance, `fake_gerrit` will hold the
1643 FakeGerritConnection object for a connection named `gerrit`.
1644
1645 :ivar FakeGearmanServer gearman_server: An instance of
1646 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1647 server that all of the Zuul components in this test use to
1648 communicate with each other.
1649
Paul Belanger174a8272017-03-14 13:20:10 -04001650 :ivar RecordingExecutorServer executor_server: An instance of
1651 :py:class:`~tests.base.RecordingExecutorServer` which is the
1652 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001653
1654 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1655 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001656 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001657 list upon completion.
1658
1659 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1660 objects representing completed builds. They are appended to
1661 the list in the order they complete.
1662
1663 """
1664
James E. Blair83005782015-12-11 14:46:03 -08001665 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001666 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001667 create_project_keys = False
James E. Blair3f876d52016-07-22 13:07:14 -07001668
1669 def _startMerger(self):
1670 self.merge_server = zuul.merger.server.MergeServer(self.config,
1671 self.connections)
1672 self.merge_server.start()
1673
Maru Newby3fe5f852015-01-13 04:22:14 +00001674 def setUp(self):
1675 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001676
1677 self.setupZK()
1678
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001679 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001680 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001681 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1682 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001683 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001684 tmp_root = tempfile.mkdtemp(
1685 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001686 self.test_root = os.path.join(tmp_root, "zuul-test")
1687 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001688 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001689 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001690 self.state_root = os.path.join(self.test_root, "lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001691
1692 if os.path.exists(self.test_root):
1693 shutil.rmtree(self.test_root)
1694 os.makedirs(self.test_root)
1695 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001696 os.makedirs(self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001697
1698 # Make per test copy of Configuration.
1699 self.setup_config()
James E. Blair59fdbac2015-12-07 17:08:06 -08001700 self.config.set('zuul', 'tenant_config',
Joshua Heskethacccffc2015-03-31 23:38:17 +11001701 os.path.join(FIXTURE_DIR,
James E. Blair59fdbac2015-12-07 17:08:06 -08001702 self.config.get('zuul', 'tenant_config')))
Monty Taylord642d852017-02-23 14:05:42 -05001703 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001704 self.config.set('executor', 'git_dir', self.executor_src_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001705 self.config.set('zuul', 'state_dir', self.state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001706
Clark Boylanb640e052014-04-03 16:41:46 -07001707 self.statsd = FakeStatsd()
Ian Wienandff977bf2015-09-30 15:38:47 +10001708 # note, use 127.0.0.1 rather than localhost to avoid getting ipv6
1709 # see: https://github.com/jsocol/pystatsd/issues/61
1710 os.environ['STATSD_HOST'] = '127.0.0.1'
Clark Boylanb640e052014-04-03 16:41:46 -07001711 os.environ['STATSD_PORT'] = str(self.statsd.port)
1712 self.statsd.start()
1713 # the statsd client object is configured in the statsd module import
Monty Taylor74fa3862016-06-02 07:39:49 +03001714 reload_module(statsd)
1715 reload_module(zuul.scheduler)
Clark Boylanb640e052014-04-03 16:41:46 -07001716
1717 self.gearman_server = FakeGearmanServer()
1718
1719 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001720 self.log.info("Gearman server on port %s" %
1721 (self.gearman_server.port,))
Clark Boylanb640e052014-04-03 16:41:46 -07001722
James E. Blaire511d2f2016-12-08 15:22:26 -08001723 gerritsource.GerritSource.replication_timeout = 1.5
1724 gerritsource.GerritSource.replication_retry_interval = 0.5
1725 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001726
Joshua Hesketh352264b2015-08-11 23:42:08 +10001727 self.sched = zuul.scheduler.Scheduler(self.config)
Clark Boylanb640e052014-04-03 16:41:46 -07001728
Jan Hruban7083edd2015-08-21 14:00:54 +02001729 self.webapp = zuul.webapp.WebApp(
1730 self.sched, port=0, listen_address='127.0.0.1')
1731
Jan Hruban6b71aff2015-10-22 16:58:08 +02001732 self.event_queues = [
1733 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08001734 self.sched.trigger_event_queue,
1735 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02001736 ]
1737
James E. Blairfef78942016-03-11 16:28:56 -08001738 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02001739 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10001740
Clark Boylanb640e052014-04-03 16:41:46 -07001741 def URLOpenerFactory(*args, **kw):
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001742 if isinstance(args[0], urllib.request.Request):
Clark Boylanb640e052014-04-03 16:41:46 -07001743 return old_urlopen(*args, **kw)
Clark Boylanb640e052014-04-03 16:41:46 -07001744 return FakeURLOpener(self.upstream_root, *args, **kw)
1745
Morgan Fainberg293f7f82016-05-30 14:01:22 -07001746 old_urlopen = urllib.request.urlopen
1747 urllib.request.urlopen = URLOpenerFactory
Clark Boylanb640e052014-04-03 16:41:46 -07001748
James E. Blair3f876d52016-07-22 13:07:14 -07001749 self._startMerger()
James E. Blair3f876d52016-07-22 13:07:14 -07001750
Paul Belanger174a8272017-03-14 13:20:10 -04001751 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08001752 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08001753 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08001754 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001755 _test_root=self.test_root,
1756 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04001757 self.executor_server.start()
1758 self.history = self.executor_server.build_history
1759 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07001760
Paul Belanger174a8272017-03-14 13:20:10 -04001761 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08001762 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001763 self.merge_client = zuul.merger.client.MergeClient(
1764 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07001765 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08001766 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05001767 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08001768
James E. Blair0d5a36e2017-02-21 10:53:44 -05001769 self.fake_nodepool = FakeNodepool(
1770 self.zk_chroot_fixture.zookeeper_host,
1771 self.zk_chroot_fixture.zookeeper_port,
1772 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07001773
Paul Belanger174a8272017-03-14 13:20:10 -04001774 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07001775 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07001776 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08001777 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07001778
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001779 self.rpc = zuul.rpclistener.RPCListener(self.config, self.sched)
Clark Boylanb640e052014-04-03 16:41:46 -07001780
1781 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001782 self.webapp.start()
1783 self.rpc.start()
Paul Belanger174a8272017-03-14 13:20:10 -04001784 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07001785 # Cleanups are run in reverse order
1786 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07001787 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07001788 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07001789
James E. Blairb9c0d772017-03-03 14:34:49 -08001790 self.sched.reconfigure(self.config)
1791 self.sched.resume()
1792
James E. Blairfef78942016-03-11 16:28:56 -08001793 def configure_connections(self):
James E. Blaire511d2f2016-12-08 15:22:26 -08001794 # Set up gerrit related fakes
1795 # Set a changes database so multiple FakeGerrit's can report back to
1796 # a virtual canonical database given by the configured hostname
1797 self.gerrit_changes_dbs = {}
1798
1799 def getGerritConnection(driver, name, config):
1800 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
1801 con = FakeGerritConnection(driver, name, config,
1802 changes_db=db,
1803 upstream_root=self.upstream_root)
1804 self.event_queues.append(con.event_queue)
1805 setattr(self, 'fake_' + name, con)
1806 return con
1807
1808 self.useFixture(fixtures.MonkeyPatch(
1809 'zuul.driver.gerrit.GerritDriver.getConnection',
1810 getGerritConnection))
1811
Gregory Haynes4fc12542015-04-22 20:38:06 -07001812 def getGithubConnection(driver, name, config):
1813 con = FakeGithubConnection(driver, name, config,
1814 upstream_root=self.upstream_root)
1815 setattr(self, 'fake_' + name, con)
1816 return con
1817
1818 self.useFixture(fixtures.MonkeyPatch(
1819 'zuul.driver.github.GithubDriver.getConnection',
1820 getGithubConnection))
1821
James E. Blaire511d2f2016-12-08 15:22:26 -08001822 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06001823 # TODO(jhesketh): This should come from lib.connections for better
1824 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10001825 # Register connections from the config
1826 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001827
Joshua Hesketh352264b2015-08-11 23:42:08 +10001828 def FakeSMTPFactory(*args, **kw):
1829 args = [self.smtp_messages] + list(args)
1830 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001831
Joshua Hesketh352264b2015-08-11 23:42:08 +10001832 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001833
James E. Blaire511d2f2016-12-08 15:22:26 -08001834 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08001835 self.connections = zuul.lib.connections.ConnectionRegistry()
James E. Blaire511d2f2016-12-08 15:22:26 -08001836 self.connections.configure(self.config)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11001837
James E. Blair83005782015-12-11 14:46:03 -08001838 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001839 # This creates the per-test configuration object. It can be
1840 # overriden by subclasses, but should not need to be since it
1841 # obeys the config_file and tenant_config_file attributes.
Clark Boylanb640e052014-04-03 16:41:46 -07001842 self.config = ConfigParser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08001843 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07001844
1845 if not self.setupSimpleLayout():
1846 if hasattr(self, 'tenant_config_file'):
1847 self.config.set('zuul', 'tenant_config',
1848 self.tenant_config_file)
1849 git_path = os.path.join(
1850 os.path.dirname(
1851 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
1852 'git')
1853 if os.path.exists(git_path):
1854 for reponame in os.listdir(git_path):
1855 project = reponame.replace('_', '/')
1856 self.copyDirToRepo(project,
1857 os.path.join(git_path, reponame))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001858 self.setupAllProjectKeys()
1859
James E. Blair06cc3922017-04-19 10:08:10 -07001860 def setupSimpleLayout(self):
1861 # If the test method has been decorated with a simple_layout,
1862 # use that instead of the class tenant_config_file. Set up a
1863 # single config-project with the specified layout, and
1864 # initialize repos for all of the 'project' entries which
1865 # appear in the layout.
1866 test_name = self.id().split('.')[-1]
1867 test = getattr(self, test_name)
1868 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07001869 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07001870 else:
1871 return False
1872
James E. Blairb70e55a2017-04-19 12:57:02 -07001873 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07001874 path = os.path.join(FIXTURE_DIR, path)
1875 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07001876 data = f.read()
1877 layout = yaml.safe_load(data)
1878 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07001879 untrusted_projects = []
1880 for item in layout:
1881 if 'project' in item:
1882 name = item['project']['name']
1883 untrusted_projects.append(name)
1884 self.init_repo(name)
1885 self.addCommitToRepo(name, 'initial commit',
1886 files={'README': ''},
1887 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07001888 if 'job' in item:
1889 jobname = item['job']['name']
1890 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07001891
1892 root = os.path.join(self.test_root, "config")
1893 if not os.path.exists(root):
1894 os.makedirs(root)
1895 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
1896 config = [{'tenant':
1897 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07001898 'source': {driver:
James E. Blair06cc3922017-04-19 10:08:10 -07001899 {'config-projects': ['common-config'],
1900 'untrusted-projects': untrusted_projects}}}}]
1901 f.write(yaml.dump(config))
1902 f.close()
1903 self.config.set('zuul', 'tenant_config',
1904 os.path.join(FIXTURE_DIR, f.name))
1905
1906 self.init_repo('common-config')
James E. Blair06cc3922017-04-19 10:08:10 -07001907 self.addCommitToRepo('common-config', 'add content from fixture',
1908 files, branch='master', tag='init')
1909
1910 return True
1911
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001912 def setupAllProjectKeys(self):
1913 if self.create_project_keys:
1914 return
1915
1916 path = self.config.get('zuul', 'tenant_config')
1917 with open(os.path.join(FIXTURE_DIR, path)) as f:
1918 tenant_config = yaml.safe_load(f.read())
1919 for tenant in tenant_config:
1920 sources = tenant['tenant']['source']
1921 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07001922 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001923 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07001924 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001925 self.setupProjectKeys(source, project)
1926
1927 def setupProjectKeys(self, source, project):
1928 # Make sure we set up an RSA key for the project so that we
1929 # don't spend time generating one:
1930
1931 key_root = os.path.join(self.state_root, 'keys')
1932 if not os.path.isdir(key_root):
1933 os.mkdir(key_root, 0o700)
1934 private_key_file = os.path.join(key_root, source, project + '.pem')
1935 private_key_dir = os.path.dirname(private_key_file)
1936 self.log.debug("Installing test keys for project %s at %s" % (
1937 project, private_key_file))
1938 if not os.path.isdir(private_key_dir):
1939 os.makedirs(private_key_dir)
1940 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1941 with open(private_key_file, 'w') as o:
1942 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08001943
James E. Blair498059b2016-12-20 13:50:13 -08001944 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001945 self.zk_chroot_fixture = self.useFixture(
1946 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05001947 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08001948 self.zk_chroot_fixture.zookeeper_host,
1949 self.zk_chroot_fixture.zookeeper_port,
1950 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08001951
James E. Blair96c6bf82016-01-15 16:20:40 -08001952 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001953 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08001954
1955 files = {}
1956 for (dirpath, dirnames, filenames) in os.walk(source_path):
1957 for filename in filenames:
1958 test_tree_filepath = os.path.join(dirpath, filename)
1959 common_path = os.path.commonprefix([test_tree_filepath,
1960 source_path])
1961 relative_filepath = test_tree_filepath[len(common_path) + 1:]
1962 with open(test_tree_filepath, 'r') as f:
1963 content = f.read()
1964 files[relative_filepath] = content
1965 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07001966 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08001967
James E. Blaire18d4602017-01-05 11:17:28 -08001968 def assertNodepoolState(self):
1969 # Make sure that there are no pending requests
1970
1971 requests = self.fake_nodepool.getNodeRequests()
1972 self.assertEqual(len(requests), 0)
1973
1974 nodes = self.fake_nodepool.getNodes()
1975 for node in nodes:
1976 self.assertFalse(node['_lock'], "Node %s is locked" %
1977 (node['_oid'],))
1978
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001979 def assertNoGeneratedKeys(self):
1980 # Make sure that Zuul did not generate any project keys
1981 # (unless it was supposed to).
1982
1983 if self.create_project_keys:
1984 return
1985
1986 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
1987 test_key = i.read()
1988
1989 key_root = os.path.join(self.state_root, 'keys')
1990 for root, dirname, files in os.walk(key_root):
1991 for fn in files:
1992 with open(os.path.join(root, fn)) as f:
1993 self.assertEqual(test_key, f.read())
1994
Clark Boylanb640e052014-04-03 16:41:46 -07001995 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07001996 self.log.debug("Assert final state")
1997 # Make sure no jobs are running
1998 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07001999 # Make sure that git.Repo objects have been garbage collected.
2000 repos = []
2001 gc.collect()
2002 for obj in gc.get_objects():
2003 if isinstance(obj, git.Repo):
James E. Blairc43525f2017-03-03 11:10:26 -08002004 self.log.debug("Leaked git repo object: %s" % repr(obj))
Clark Boylanb640e052014-04-03 16:41:46 -07002005 repos.append(obj)
2006 self.assertEqual(len(repos), 0)
2007 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002008 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002009 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002010 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002011 for tenant in self.sched.abide.tenants.values():
2012 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002013 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002014 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002015
2016 def shutdown(self):
2017 self.log.debug("Shutting down after tests")
Paul Belanger174a8272017-03-14 13:20:10 -04002018 self.executor_client.stop()
James E. Blair3f876d52016-07-22 13:07:14 -07002019 self.merge_server.stop()
2020 self.merge_server.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002021 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002022 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002023 self.sched.stop()
2024 self.sched.join()
2025 self.statsd.stop()
2026 self.statsd.join()
2027 self.webapp.stop()
2028 self.webapp.join()
2029 self.rpc.stop()
2030 self.rpc.join()
2031 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002032 self.fake_nodepool.stop()
2033 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002034 self.printHistory()
Clark Boylanf18e3b82017-04-24 17:34:13 -07002035 # we whitelist watchdog threads as they have relatively long delays
2036 # before noticing they should exit, but they should exit on their own.
2037 threads = [t for t in threading.enumerate()
2038 if t.name != 'executor-watchdog']
Clark Boylanb640e052014-04-03 16:41:46 -07002039 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002040 log_str = ""
2041 for thread_id, stack_frame in sys._current_frames().items():
2042 log_str += "Thread: %s\n" % thread_id
2043 log_str += "".join(traceback.format_stack(stack_frame))
2044 self.log.debug(log_str)
2045 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002046
James E. Blaira002b032017-04-18 10:35:48 -07002047 def assertCleanShutdown(self):
2048 pass
2049
James E. Blairc4ba97a2017-04-19 16:26:24 -07002050 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002051 parts = project.split('/')
2052 path = os.path.join(self.upstream_root, *parts[:-1])
2053 if not os.path.exists(path):
2054 os.makedirs(path)
2055 path = os.path.join(self.upstream_root, project)
2056 repo = git.Repo.init(path)
2057
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002058 with repo.config_writer() as config_writer:
2059 config_writer.set_value('user', 'email', 'user@example.com')
2060 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002061
Clark Boylanb640e052014-04-03 16:41:46 -07002062 repo.index.commit('initial commit')
2063 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002064 if tag:
2065 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002066
James E. Blair97d902e2014-08-21 13:25:56 -07002067 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002068 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002069 repo.git.clean('-x', '-f', '-d')
2070
James E. Blair97d902e2014-08-21 13:25:56 -07002071 def create_branch(self, project, branch):
2072 path = os.path.join(self.upstream_root, project)
2073 repo = git.Repo.init(path)
2074 fn = os.path.join(path, 'README')
2075
2076 branch_head = repo.create_head(branch)
2077 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002078 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002079 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002080 f.close()
2081 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002082 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002083
James E. Blair97d902e2014-08-21 13:25:56 -07002084 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002085 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002086 repo.git.clean('-x', '-f', '-d')
2087
Sachi King9f16d522016-03-16 12:20:45 +11002088 def create_commit(self, project):
2089 path = os.path.join(self.upstream_root, project)
2090 repo = git.Repo(path)
2091 repo.head.reference = repo.heads['master']
2092 file_name = os.path.join(path, 'README')
2093 with open(file_name, 'a') as f:
2094 f.write('creating fake commit\n')
2095 repo.index.add([file_name])
2096 commit = repo.index.commit('Creating a fake commit')
2097 return commit.hexsha
2098
James E. Blairf4a5f022017-04-18 14:01:10 -07002099 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002100 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002101 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002102 while len(self.builds):
2103 self.release(self.builds[0])
2104 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002105 i += 1
2106 if count is not None and i >= count:
2107 break
James E. Blairb8c16472015-05-05 14:55:26 -07002108
Clark Boylanb640e052014-04-03 16:41:46 -07002109 def release(self, job):
2110 if isinstance(job, FakeBuild):
2111 job.release()
2112 else:
2113 job.waiting = False
2114 self.log.debug("Queued job %s released" % job.unique)
2115 self.gearman_server.wakeConnections()
2116
2117 def getParameter(self, job, name):
2118 if isinstance(job, FakeBuild):
2119 return job.parameters[name]
2120 else:
2121 parameters = json.loads(job.arguments)
2122 return parameters[name]
2123
Clark Boylanb640e052014-04-03 16:41:46 -07002124 def haveAllBuildsReported(self):
2125 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002126 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002127 return False
2128 # Find out if every build that the worker has completed has been
2129 # reported back to Zuul. If it hasn't then that means a Gearman
2130 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002131 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002132 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002133 if not zbuild:
2134 # It has already been reported
2135 continue
2136 # It hasn't been reported yet.
2137 return False
2138 # Make sure that none of the worker connections are in GRAB_WAIT
Paul Belanger174a8272017-03-14 13:20:10 -04002139 for connection in self.executor_server.worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002140 if connection.state == 'GRAB_WAIT':
2141 return False
2142 return True
2143
2144 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002145 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002146 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002147 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002148 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002149 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002150 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002151 for j in conn.related_jobs.values():
2152 if j.unique == build.uuid:
2153 client_job = j
2154 break
2155 if not client_job:
2156 self.log.debug("%s is not known to the gearman client" %
2157 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002158 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002159 if not client_job.handle:
2160 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002161 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002162 server_job = self.gearman_server.jobs.get(client_job.handle)
2163 if not server_job:
2164 self.log.debug("%s is not known to the gearman server" %
2165 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002166 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002167 if not hasattr(server_job, 'waiting'):
2168 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002169 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002170 if server_job.waiting:
2171 continue
James E. Blair17302972016-08-10 16:11:42 -07002172 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002173 self.log.debug("%s has not reported start" % build)
2174 return False
Paul Belanger174a8272017-03-14 13:20:10 -04002175 worker_build = self.executor_server.job_builds.get(
2176 server_job.unique)
James E. Blair962220f2016-08-03 11:22:38 -07002177 if worker_build:
2178 if worker_build.isWaiting():
2179 continue
2180 else:
2181 self.log.debug("%s is running" % worker_build)
2182 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002183 else:
James E. Blair962220f2016-08-03 11:22:38 -07002184 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002185 return False
James E. Blaira002b032017-04-18 10:35:48 -07002186 for (build_uuid, job_worker) in \
2187 self.executor_server.job_workers.items():
2188 if build_uuid not in seen_builds:
2189 self.log.debug("%s is not finalized" % build_uuid)
2190 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002191 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002192
James E. Blairdce6cea2016-12-20 16:45:32 -08002193 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002194 if self.fake_nodepool.paused:
2195 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002196 if self.sched.nodepool.requests:
2197 return False
2198 return True
2199
Jan Hruban6b71aff2015-10-22 16:58:08 +02002200 def eventQueuesEmpty(self):
2201 for queue in self.event_queues:
2202 yield queue.empty()
2203
2204 def eventQueuesJoin(self):
2205 for queue in self.event_queues:
2206 queue.join()
2207
Clark Boylanb640e052014-04-03 16:41:46 -07002208 def waitUntilSettled(self):
2209 self.log.debug("Waiting until settled...")
2210 start = time.time()
2211 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002212 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002213 self.log.error("Timeout waiting for Zuul to settle")
2214 self.log.error("Queue status:")
James E. Blair622c9682016-06-09 08:14:53 -07002215 for queue in self.event_queues:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002216 self.log.error(" %s: %s" % (queue, queue.empty()))
2217 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002218 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002219 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002220 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002221 self.log.error("All requests completed: %s" %
2222 (self.areAllNodeRequestsComplete(),))
2223 self.log.error("Merge client jobs: %s" %
2224 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002225 raise Exception("Timeout waiting for Zuul to settle")
2226 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002227
Paul Belanger174a8272017-03-14 13:20:10 -04002228 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002229 # have all build states propogated to zuul?
2230 if self.haveAllBuildsReported():
2231 # Join ensures that the queue is empty _and_ events have been
2232 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002233 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002234 self.sched.run_handler_lock.acquire()
James E. Blair14abdf42015-12-09 16:11:53 -08002235 if (not self.merge_client.jobs and
Clark Boylanb640e052014-04-03 16:41:46 -07002236 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002237 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002238 self.areAllNodeRequestsComplete() and
2239 all(self.eventQueuesEmpty())):
2240 # The queue empty check is placed at the end to
2241 # ensure that if a component adds an event between
2242 # when locked the run handler and checked that the
2243 # components were stable, we don't erroneously
2244 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002245 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002246 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002247 self.log.debug("...settled.")
2248 return
2249 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002250 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002251 self.sched.wake_event.wait(0.1)
2252
2253 def countJobResults(self, jobs, result):
2254 jobs = filter(lambda x: x.result == result, jobs)
2255 return len(jobs)
2256
James E. Blair96c6bf82016-01-15 16:20:40 -08002257 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002258 for job in self.history:
2259 if (job.name == name and
2260 (project is None or
2261 job.parameters['ZUUL_PROJECT'] == project)):
2262 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002263 raise Exception("Unable to find job %s in history" % name)
2264
2265 def assertEmptyQueues(self):
2266 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002267 for tenant in self.sched.abide.tenants.values():
2268 for pipeline in tenant.layout.pipelines.values():
2269 for queue in pipeline.queues:
2270 if len(queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002271 print('pipeline %s queue %s contents %s' % (
2272 pipeline.name, queue.name, queue.queue))
James E. Blair59fdbac2015-12-07 17:08:06 -08002273 self.assertEqual(len(queue.queue), 0,
2274 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002275
2276 def assertReportedStat(self, key, value=None, kind=None):
2277 start = time.time()
2278 while time.time() < (start + 5):
2279 for stat in self.statsd.stats:
Clark Boylanb640e052014-04-03 16:41:46 -07002280 k, v = stat.split(':')
2281 if key == k:
2282 if value is None and kind is None:
2283 return
2284 elif value:
2285 if value == v:
2286 return
2287 elif kind:
2288 if v.endswith('|' + kind):
2289 return
2290 time.sleep(0.1)
2291
Clark Boylanb640e052014-04-03 16:41:46 -07002292 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002293
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002294 def assertBuilds(self, builds):
2295 """Assert that the running builds are as described.
2296
2297 The list of running builds is examined and must match exactly
2298 the list of builds described by the input.
2299
2300 :arg list builds: A list of dictionaries. Each item in the
2301 list must match the corresponding build in the build
2302 history, and each element of the dictionary must match the
2303 corresponding attribute of the build.
2304
2305 """
James E. Blair3158e282016-08-19 09:34:11 -07002306 try:
2307 self.assertEqual(len(self.builds), len(builds))
2308 for i, d in enumerate(builds):
2309 for k, v in d.items():
2310 self.assertEqual(
2311 getattr(self.builds[i], k), v,
2312 "Element %i in builds does not match" % (i,))
2313 except Exception:
2314 for build in self.builds:
2315 self.log.error("Running build: %s" % build)
2316 else:
2317 self.log.error("No running builds")
2318 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002319
James E. Blairb536ecc2016-08-31 10:11:42 -07002320 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002321 """Assert that the completed builds are as described.
2322
2323 The list of completed builds is examined and must match
2324 exactly the list of builds described by the input.
2325
2326 :arg list history: A list of dictionaries. Each item in the
2327 list must match the corresponding build in the build
2328 history, and each element of the dictionary must match the
2329 corresponding attribute of the build.
2330
James E. Blairb536ecc2016-08-31 10:11:42 -07002331 :arg bool ordered: If true, the history must match the order
2332 supplied, if false, the builds are permitted to have
2333 arrived in any order.
2334
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002335 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002336 def matches(history_item, item):
2337 for k, v in item.items():
2338 if getattr(history_item, k) != v:
2339 return False
2340 return True
James E. Blair3158e282016-08-19 09:34:11 -07002341 try:
2342 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002343 if ordered:
2344 for i, d in enumerate(history):
2345 if not matches(self.history[i], d):
2346 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002347 "Element %i in history does not match %s" %
2348 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002349 else:
2350 unseen = self.history[:]
2351 for i, d in enumerate(history):
2352 found = False
2353 for unseen_item in unseen:
2354 if matches(unseen_item, d):
2355 found = True
2356 unseen.remove(unseen_item)
2357 break
2358 if not found:
2359 raise Exception("No match found for element %i "
2360 "in history" % (i,))
2361 if unseen:
2362 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002363 except Exception:
2364 for build in self.history:
2365 self.log.error("Completed build: %s" % build)
2366 else:
2367 self.log.error("No completed builds")
2368 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002369
James E. Blair6ac368c2016-12-22 18:07:20 -08002370 def printHistory(self):
2371 """Log the build history.
2372
2373 This can be useful during tests to summarize what jobs have
2374 completed.
2375
2376 """
2377 self.log.debug("Build history:")
2378 for build in self.history:
2379 self.log.debug(build)
2380
James E. Blair59fdbac2015-12-07 17:08:06 -08002381 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002382 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2383
James E. Blair9ea70072017-04-19 16:05:30 -07002384 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002385 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002386 if not os.path.exists(root):
2387 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002388 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2389 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002390- tenant:
2391 name: openstack
2392 source:
2393 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002394 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002395 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002396 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002397 - org/project
2398 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002399 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002400 f.close()
Paul Belanger66e95962016-11-11 12:11:06 -05002401 self.config.set('zuul', 'tenant_config',
2402 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002403 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002404
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002405 def addCommitToRepo(self, project, message, files,
2406 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002407 path = os.path.join(self.upstream_root, project)
2408 repo = git.Repo(path)
2409 repo.head.reference = branch
2410 zuul.merger.merger.reset_repo_to_head(repo)
2411 for fn, content in files.items():
2412 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002413 try:
2414 os.makedirs(os.path.dirname(fn))
2415 except OSError:
2416 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002417 with open(fn, 'w') as f:
2418 f.write(content)
2419 repo.index.add([fn])
2420 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002421 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002422 repo.heads[branch].commit = commit
2423 repo.head.reference = branch
2424 repo.git.clean('-x', '-f', '-d')
2425 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002426 if tag:
2427 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002428 return before
2429
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002430 def commitConfigUpdate(self, project_name, source_name):
2431 """Commit an update to zuul.yaml
2432
2433 This overwrites the zuul.yaml in the specificed project with
2434 the contents specified.
2435
2436 :arg str project_name: The name of the project containing
2437 zuul.yaml (e.g., common-config)
2438
2439 :arg str source_name: The path to the file (underneath the
2440 test fixture directory) whose contents should be used to
2441 replace zuul.yaml.
2442 """
2443
2444 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002445 files = {}
2446 with open(source_path, 'r') as f:
2447 data = f.read()
2448 layout = yaml.safe_load(data)
2449 files['zuul.yaml'] = data
2450 for item in layout:
2451 if 'job' in item:
2452 jobname = item['job']['name']
2453 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002454 before = self.addCommitToRepo(
2455 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002456 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002457 return before
2458
James E. Blair7fc8daa2016-08-08 15:37:15 -07002459 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002460
James E. Blair7fc8daa2016-08-08 15:37:15 -07002461 """Inject a Fake (Gerrit) event.
2462
2463 This method accepts a JSON-encoded event and simulates Zuul
2464 having received it from Gerrit. It could (and should)
2465 eventually apply to any connection type, but is currently only
2466 used with Gerrit connections. The name of the connection is
2467 used to look up the corresponding server, and the event is
2468 simulated as having been received by all Zuul connections
2469 attached to that server. So if two Gerrit connections in Zuul
2470 are connected to the same Gerrit server, and you invoke this
2471 method specifying the name of one of them, the event will be
2472 received by both.
2473
2474 .. note::
2475
2476 "self.fake_gerrit.addEvent" calls should be migrated to
2477 this method.
2478
2479 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002480 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002481 :arg str event: The JSON-encoded event.
2482
2483 """
2484 specified_conn = self.connections.connections[connection]
2485 for conn in self.connections.connections.values():
2486 if (isinstance(conn, specified_conn.__class__) and
2487 specified_conn.server == conn.server):
2488 conn.addEvent(event)
2489
James E. Blair3f876d52016-07-22 13:07:14 -07002490
2491class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002492 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002493 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002494
Joshua Heskethd78b4482015-09-14 16:56:34 -06002495
2496class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002497 def setup_config(self):
2498 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002499 for section_name in self.config.sections():
2500 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2501 section_name, re.I)
2502 if not con_match:
2503 continue
2504
2505 if self.config.get(section_name, 'driver') == 'sql':
2506 f = MySQLSchemaFixture()
2507 self.useFixture(f)
2508 if (self.config.get(section_name, 'dburi') ==
2509 '$MYSQL_FIXTURE_DBURI$'):
2510 self.config.set(section_name, 'dburi', f.dburi)