blob: 70889bbfe68e4f4597fe296d5a7225d28f832fbb [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
Paul Belanger174a8272017-03-14 13:20:10 -040060import zuul.executor.server
61import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020066import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070067import zuul.nodepool
Jesse Keating80730e62017-09-14 15:35:11 -060068import zuul.rpcclient
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Jesse Keating80730e62017-09-14 15:35:11 -060072from zuul.lib.config import get_default
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200136 categories = {'Approved': ('Approved', -1, 1),
137 'Code-Review': ('Code-Review', -2, 2),
138 'Verified': ('Verified', -2, 2)}
139
Clark Boylanb640e052014-04-03 16:41:46 -0700140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700141 status='NEW', upstream_root=None, files={},
142 parent=None):
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': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800174 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700175
176 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700177 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair289f5932017-07-27 15:02:29 -0700181 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700184 if parent is None:
185 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700188 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700197 if content is None:
198 os.unlink(fn)
199 repo.index.remove([fn])
200 else:
201 d = os.path.dirname(fn)
202 if not os.path.exists(d):
203 os.makedirs(d)
204 with open(fn, 'w') as f:
205 f.write(content)
206 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700207 else:
208 for fni in range(100):
209 fn = os.path.join(path, str(fni))
210 f = open(fn, 'w')
211 for ci in range(4096):
212 f.write(random.choice(string.printable))
213 f.close()
214 repo.index.add([fn])
215
216 r = repo.index.commit(msg)
217 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700218 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700219 repo.git.clean('-x', '-f', '-d')
220 repo.heads['master'].checkout()
221 return r
222
James E. Blair289f5932017-07-27 15:02:29 -0700223 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700224 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700225 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700226 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 data = ("test %s %s %s\n" %
228 (self.branch, self.number, self.latest_patchset))
229 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700230 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700231 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700232 ps_files = [{'file': '/COMMIT_MSG',
233 'type': 'ADDED'},
234 {'file': 'README',
235 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700236 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700237 ps_files.append({'file': f, 'type': 'ADDED'})
238 d = {'approvals': [],
239 'createdOn': time.time(),
240 'files': ps_files,
241 'number': str(self.latest_patchset),
242 'ref': 'refs/changes/1/%s/%s' % (self.number,
243 self.latest_patchset),
244 'revision': c.hexsha,
245 'uploader': {'email': 'user@example.com',
246 'name': 'User name',
247 'username': 'user'}}
248 self.data['currentPatchSet'] = d
249 self.patchsets.append(d)
250 self.data['submitRecords'] = self.getSubmitRecords()
251
252 def getPatchsetCreatedEvent(self, patchset):
253 event = {"type": "patchset-created",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "uploader": {"name": "User Name"}}
263 return event
264
265 def getChangeRestoredEvent(self):
266 event = {"type": "change-restored",
267 "change": {"project": self.project,
268 "branch": self.branch,
269 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
270 "number": str(self.number),
271 "subject": self.subject,
272 "owner": {"name": "User Name"},
273 "url": "https://hostname/3"},
274 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100275 "patchSet": self.patchsets[-1],
276 "reason": ""}
277 return event
278
279 def getChangeAbandonedEvent(self):
280 event = {"type": "change-abandoned",
281 "change": {"project": self.project,
282 "branch": self.branch,
283 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
284 "number": str(self.number),
285 "subject": self.subject,
286 "owner": {"name": "User Name"},
287 "url": "https://hostname/3"},
288 "abandoner": {"name": "User Name"},
289 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700290 "reason": ""}
291 return event
292
293 def getChangeCommentEvent(self, patchset):
294 event = {"type": "comment-added",
295 "change": {"project": self.project,
296 "branch": self.branch,
297 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
298 "number": str(self.number),
299 "subject": self.subject,
300 "owner": {"name": "User Name"},
301 "url": "https://hostname/3"},
302 "patchSet": self.patchsets[patchset - 1],
303 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200304 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700305 "description": "Code-Review",
306 "value": "0"}],
307 "comment": "This is a comment"}
308 return event
309
James E. Blairc2a5ed72017-02-20 14:12:01 -0500310 def getChangeMergedEvent(self):
311 event = {"submitter": {"name": "Jenkins",
312 "username": "jenkins"},
313 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
314 "patchSet": self.patchsets[-1],
315 "change": self.data,
316 "type": "change-merged",
317 "eventCreatedOn": 1487613810}
318 return event
319
James E. Blair8cce42e2016-10-18 08:18:36 -0700320 def getRefUpdatedEvent(self):
321 path = os.path.join(self.upstream_root, self.project)
322 repo = git.Repo(path)
323 oldrev = repo.heads[self.branch].commit.hexsha
324
325 event = {
326 "type": "ref-updated",
327 "submitter": {
328 "name": "User Name",
329 },
330 "refUpdate": {
331 "oldRev": oldrev,
332 "newRev": self.patchsets[-1]['revision'],
333 "refName": self.branch,
334 "project": self.project,
335 }
336 }
337 return event
338
Joshua Hesketh642824b2014-07-01 17:54:59 +1000339 def addApproval(self, category, value, username='reviewer_john',
340 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700341 if not granted_on:
342 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000343 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200344 'description': self.categories[category][0],
345 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000346 'value': str(value),
347 'by': {
348 'username': username,
349 'email': username + '@example.com',
350 },
351 'grantedOn': int(granted_on)
352 }
Clark Boylanb640e052014-04-03 16:41:46 -0700353 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200354 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700355 del self.patchsets[-1]['approvals'][i]
356 self.patchsets[-1]['approvals'].append(approval)
357 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'author': {'email': 'author@example.com',
359 'name': 'Patchset Author',
360 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700361 'change': {'branch': self.branch,
362 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
363 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'owner': {'email': 'owner@example.com',
365 'name': 'Change Owner',
366 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700367 'project': self.project,
368 'subject': self.subject,
369 'topic': 'master',
370 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000371 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700372 'patchSet': self.patchsets[-1],
373 'type': 'comment-added'}
374 self.data['submitRecords'] = self.getSubmitRecords()
375 return json.loads(json.dumps(event))
376
377 def getSubmitRecords(self):
378 status = {}
379 for cat in self.categories.keys():
380 status[cat] = 0
381
382 for a in self.patchsets[-1]['approvals']:
383 cur = status[a['type']]
384 cat_min, cat_max = self.categories[a['type']][1:]
385 new = int(a['value'])
386 if new == cat_min:
387 cur = new
388 elif abs(new) > abs(cur):
389 cur = new
390 status[a['type']] = cur
391
392 labels = []
393 ok = True
394 for typ, cat in self.categories.items():
395 cur = status[typ]
396 cat_min, cat_max = cat[1:]
397 if cur == cat_min:
398 value = 'REJECT'
399 ok = False
400 elif cur == cat_max:
401 value = 'OK'
402 else:
403 value = 'NEED'
404 ok = False
405 labels.append({'label': cat[0], 'status': value})
406 if ok:
407 return [{'status': 'OK'}]
408 return [{'status': 'NOT_READY',
409 'labels': labels}]
410
411 def setDependsOn(self, other, patchset):
412 self.depends_on_change = other
413 d = {'id': other.data['id'],
414 'number': other.data['number'],
415 'ref': other.patchsets[patchset - 1]['ref']
416 }
417 self.data['dependsOn'] = [d]
418
419 other.needed_by_changes.append(self)
420 needed = other.data.get('neededBy', [])
421 d = {'id': self.data['id'],
422 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700423 'ref': self.patchsets[-1]['ref'],
424 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700425 }
426 needed.append(d)
427 other.data['neededBy'] = needed
428
429 def query(self):
430 self.queried += 1
431 d = self.data.get('dependsOn')
432 if d:
433 d = d[0]
434 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
435 d['isCurrentPatchSet'] = True
436 else:
437 d['isCurrentPatchSet'] = False
438 return json.loads(json.dumps(self.data))
439
440 def setMerged(self):
441 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000442 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700443 return
444 if self.fail_merge:
445 return
446 self.data['status'] = 'MERGED'
447 self.open = False
448
449 path = os.path.join(self.upstream_root, self.project)
450 repo = git.Repo(path)
451 repo.heads[self.branch].commit = \
452 repo.commit(self.patchsets[-1]['revision'])
453
454 def setReported(self):
455 self.reported += 1
456
457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700459 """A Fake Gerrit connection for use in tests.
460
461 This subclasses
462 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
463 ability for tests to add changes to the fake Gerrit it represents.
464 """
465
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700467
James E. Blaire511d2f2016-12-08 15:22:26 -0800468 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700469 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800470 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000471 connection_config)
472
Monty Taylorb934c1a2017-06-16 19:31:47 -0500473 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
475 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000476 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700477 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200478 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700479
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700480 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700481 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700482 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700483 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700484 c = FakeGerritChange(self, self.change_number, project, branch,
485 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700486 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700487 self.changes[self.change_number] = c
488 return c
489
James E. Blair1edfd972017-12-01 15:54:24 -0800490 def addFakeTag(self, project, branch, tag):
491 path = os.path.join(self.upstream_root, project)
492 repo = git.Repo(path)
493 commit = repo.heads[branch].commit
494 newrev = commit.hexsha
495 ref = 'refs/tags/' + tag
496
497 git.Tag.create(repo, tag, commit)
498
499 event = {
500 "type": "ref-updated",
501 "submitter": {
502 "name": "User Name",
503 },
504 "refUpdate": {
505 "oldRev": 40 * '0',
506 "newRev": newrev,
507 "refName": ref,
508 "project": project,
509 }
510 }
511 return event
512
James E. Blair72facdc2017-08-17 10:29:12 -0700513 def getFakeBranchCreatedEvent(self, project, branch):
514 path = os.path.join(self.upstream_root, project)
515 repo = git.Repo(path)
516 oldrev = 40 * '0'
517
518 event = {
519 "type": "ref-updated",
520 "submitter": {
521 "name": "User Name",
522 },
523 "refUpdate": {
524 "oldRev": oldrev,
525 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700526 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700527 "project": project,
528 }
529 }
530 return event
531
Clark Boylanb640e052014-04-03 16:41:46 -0700532 def review(self, project, changeid, message, action):
533 number, ps = changeid.split(',')
534 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000535
536 # Add the approval back onto the change (ie simulate what gerrit would
537 # do).
538 # Usually when zuul leaves a review it'll create a feedback loop where
539 # zuul's review enters another gerrit event (which is then picked up by
540 # zuul). However, we can't mimic this behaviour (by adding this
541 # approval event into the queue) as it stops jobs from checking what
542 # happens before this event is triggered. If a job needs to see what
543 # happens they can add their own verified event into the queue.
544 # Nevertheless, we can update change with the new review in gerrit.
545
James E. Blair8b5408c2016-08-08 15:37:46 -0700546 for cat in action.keys():
547 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000548 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000549
Clark Boylanb640e052014-04-03 16:41:46 -0700550 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000551
Clark Boylanb640e052014-04-03 16:41:46 -0700552 if 'submit' in action:
553 change.setMerged()
554 if message:
555 change.setReported()
556
557 def query(self, number):
558 change = self.changes.get(int(number))
559 if change:
560 return change.query()
561 return {}
562
James E. Blair0e4c7912018-01-02 14:20:17 -0800563 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800564 if query.startswith('change:'):
565 # Query a specific changeid
566 changeid = query[len('change:'):]
567 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800568 if (change.data['id'] == changeid or
569 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700570 elif query.startswith('message:'):
571 # Query the content of a commit message
572 msg = query[len('message:'):].strip()
573 l = [change.query() for change in self.changes.values()
574 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800575 else:
576 # Query all open changes
577 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700578 return l
James E. Blairc494d542014-08-06 09:23:52 -0700579
James E. Blair0e4c7912018-01-02 14:20:17 -0800580 def simpleQuery(self, query):
581 self.log.debug("simpleQuery: %s" % query)
582 self.queries.append(query)
583 results = []
584 if query.startswith('(') and 'OR' in query:
585 query = query[1:-2]
586 for q in query.split(' OR '):
587 for r in self._simpleQuery(q):
588 if r not in results:
589 results.append(r)
590 else:
591 results = self._simpleQuery(query)
592 return results
593
Joshua Hesketh352264b2015-08-11 23:42:08 +1000594 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700595 pass
596
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200597 def _uploadPack(self, project):
598 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
599 'multi_ack thin-pack side-band side-band-64k ofs-delta '
600 'shallow no-progress include-tag multi_ack_detailed no-done\n')
601 path = os.path.join(self.upstream_root, project.name)
602 repo = git.Repo(path)
603 for ref in repo.refs:
604 r = ref.object.hexsha + ' ' + ref.path + '\n'
605 ret += '%04x%s' % (len(r) + 4, r)
606 ret += '0000'
607 return ret
608
Joshua Hesketh352264b2015-08-11 23:42:08 +1000609 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800610 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000611
Clark Boylanb640e052014-04-03 16:41:46 -0700612
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613class GithubChangeReference(git.Reference):
614 _common_path_default = "refs/pull"
615 _points_to_commits_only = True
616
617
618class FakeGithubPullRequest(object):
619
620 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800621 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700622 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700623 """Creates a new PR with several commits.
624 Sends an event about opened PR."""
625 self.github = github
626 self.source = github
627 self.number = number
628 self.project = project
629 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100630 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700631 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100632 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700633 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100634 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700635 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100636 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100637 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800638 self.reviews = []
639 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700640 self.updated_at = None
641 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100642 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100643 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700644 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800645 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700646 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100647 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700648 self._updateTimeStamp()
649
Jan Hruban570d01c2016-03-10 21:51:32 +0100650 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700651 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100652 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700653 self._updateTimeStamp()
654
Jan Hruban570d01c2016-03-10 21:51:32 +0100655 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700656 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100657 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700658 self._updateTimeStamp()
659
660 def getPullRequestOpenedEvent(self):
661 return self._getPullRequestEvent('opened')
662
663 def getPullRequestSynchronizeEvent(self):
664 return self._getPullRequestEvent('synchronize')
665
666 def getPullRequestReopenedEvent(self):
667 return self._getPullRequestEvent('reopened')
668
669 def getPullRequestClosedEvent(self):
670 return self._getPullRequestEvent('closed')
671
Jesse Keatinga41566f2017-06-14 18:17:51 -0700672 def getPullRequestEditedEvent(self):
673 return self._getPullRequestEvent('edited')
674
Gregory Haynes4fc12542015-04-22 20:38:06 -0700675 def addComment(self, message):
676 self.comments.append(message)
677 self._updateTimeStamp()
678
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200679 def getCommentAddedEvent(self, text):
680 name = 'issue_comment'
681 data = {
682 'action': 'created',
683 'issue': {
684 'number': self.number
685 },
686 'comment': {
687 'body': text
688 },
689 'repository': {
690 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100691 },
692 'sender': {
693 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200694 }
695 }
696 return (name, data)
697
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800698 def getReviewAddedEvent(self, review):
699 name = 'pull_request_review'
700 data = {
701 'action': 'submitted',
702 'pull_request': {
703 'number': self.number,
704 'title': self.subject,
705 'updated_at': self.updated_at,
706 'base': {
707 'ref': self.branch,
708 'repo': {
709 'full_name': self.project
710 }
711 },
712 'head': {
713 'sha': self.head_sha
714 }
715 },
716 'review': {
717 'state': review
718 },
719 'repository': {
720 'full_name': self.project
721 },
722 'sender': {
723 'login': 'ghuser'
724 }
725 }
726 return (name, data)
727
Jan Hruban16ad31f2015-11-07 14:39:07 +0100728 def addLabel(self, name):
729 if name not in self.labels:
730 self.labels.append(name)
731 self._updateTimeStamp()
732 return self._getLabelEvent(name)
733
734 def removeLabel(self, name):
735 if name in self.labels:
736 self.labels.remove(name)
737 self._updateTimeStamp()
738 return self._getUnlabelEvent(name)
739
740 def _getLabelEvent(self, label):
741 name = 'pull_request'
742 data = {
743 'action': 'labeled',
744 'pull_request': {
745 'number': self.number,
746 'updated_at': self.updated_at,
747 'base': {
748 'ref': self.branch,
749 'repo': {
750 'full_name': self.project
751 }
752 },
753 'head': {
754 'sha': self.head_sha
755 }
756 },
757 'label': {
758 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100759 },
760 'sender': {
761 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100762 }
763 }
764 return (name, data)
765
766 def _getUnlabelEvent(self, label):
767 name = 'pull_request'
768 data = {
769 'action': 'unlabeled',
770 'pull_request': {
771 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100772 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100773 'updated_at': self.updated_at,
774 'base': {
775 'ref': self.branch,
776 'repo': {
777 'full_name': self.project
778 }
779 },
780 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800781 'sha': self.head_sha,
782 'repo': {
783 'full_name': self.project
784 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100785 }
786 },
787 'label': {
788 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100789 },
790 'sender': {
791 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100792 }
793 }
794 return (name, data)
795
Jesse Keatinga41566f2017-06-14 18:17:51 -0700796 def editBody(self, body):
797 self.body = body
798 self._updateTimeStamp()
799
Gregory Haynes4fc12542015-04-22 20:38:06 -0700800 def _getRepo(self):
801 repo_path = os.path.join(self.upstream_root, self.project)
802 return git.Repo(repo_path)
803
804 def _createPRRef(self):
805 repo = self._getRepo()
806 GithubChangeReference.create(
807 repo, self._getPRReference(), 'refs/tags/init')
808
Jan Hruban570d01c2016-03-10 21:51:32 +0100809 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 repo = self._getRepo()
811 ref = repo.references[self._getPRReference()]
812 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100813 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100815 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 repo.head.reference = ref
817 zuul.merger.merger.reset_repo_to_head(repo)
818 repo.git.clean('-x', '-f', '-d')
819
Jan Hruban570d01c2016-03-10 21:51:32 +0100820 if files:
821 fn = files[0]
822 self.files = files
823 else:
824 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
825 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100826 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700827 fn = os.path.join(repo.working_dir, fn)
828 f = open(fn, 'w')
829 with open(fn, 'w') as f:
830 f.write("test %s %s\n" %
831 (self.branch, self.number))
832 repo.index.add([fn])
833
834 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800835 # Create an empty set of statuses for the given sha,
836 # each sha on a PR may have a status set on it
837 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 repo.head.reference = 'master'
839 zuul.merger.merger.reset_repo_to_head(repo)
840 repo.git.clean('-x', '-f', '-d')
841 repo.heads['master'].checkout()
842
843 def _updateTimeStamp(self):
844 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
845
846 def getPRHeadSha(self):
847 repo = self._getRepo()
848 return repo.references[self._getPRReference()].commit.hexsha
849
Jesse Keatingae4cd272017-01-30 17:10:44 -0800850 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800851 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
852 # convert the timestamp to a str format that would be returned
853 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800854
Adam Gandelmand81dd762017-02-09 15:15:49 -0800855 if granted_on:
856 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
857 submitted_at = time.strftime(
858 gh_time_format, granted_on.timetuple())
859 else:
860 # github timestamps only down to the second, so we need to make
861 # sure reviews that tests add appear to be added over a period of
862 # time in the past and not all at once.
863 if not self.reviews:
864 # the first review happens 10 mins ago
865 offset = 600
866 else:
867 # subsequent reviews happen 1 minute closer to now
868 offset = 600 - (len(self.reviews) * 60)
869
870 granted_on = datetime.datetime.utcfromtimestamp(
871 time.time() - offset)
872 submitted_at = time.strftime(
873 gh_time_format, granted_on.timetuple())
874
Jesse Keatingae4cd272017-01-30 17:10:44 -0800875 self.reviews.append({
876 'state': state,
877 'user': {
878 'login': user,
879 'email': user + "@derp.com",
880 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800881 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800882 })
883
Gregory Haynes4fc12542015-04-22 20:38:06 -0700884 def _getPRReference(self):
885 return '%s/head' % self.number
886
887 def _getPullRequestEvent(self, action):
888 name = 'pull_request'
889 data = {
890 'action': action,
891 'number': self.number,
892 'pull_request': {
893 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100894 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700895 'updated_at': self.updated_at,
896 'base': {
897 'ref': self.branch,
898 'repo': {
899 'full_name': self.project
900 }
901 },
902 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800903 'sha': self.head_sha,
904 'repo': {
905 'full_name': self.project
906 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700907 },
908 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100909 },
910 'sender': {
911 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 }
913 }
914 return (name, data)
915
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800916 def getCommitStatusEvent(self, context, state='success', user='zuul'):
917 name = 'status'
918 data = {
919 'state': state,
920 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700921 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800922 'description': 'Test results for %s: %s' % (self.head_sha, state),
923 'target_url': 'http://zuul/%s' % self.head_sha,
924 'branches': [],
925 'context': context,
926 'sender': {
927 'login': user
928 }
929 }
930 return (name, data)
931
James E. Blair289f5932017-07-27 15:02:29 -0700932 def setMerged(self, commit_message):
933 self.is_merged = True
934 self.merge_message = commit_message
935
936 repo = self._getRepo()
937 repo.heads[self.branch].commit = repo.commit(self.head_sha)
938
Gregory Haynes4fc12542015-04-22 20:38:06 -0700939
940class FakeGithubConnection(githubconnection.GithubConnection):
941 log = logging.getLogger("zuul.test.FakeGithubConnection")
942
Jesse Keating80730e62017-09-14 15:35:11 -0600943 def __init__(self, driver, connection_name, connection_config, rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -0800944 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700945 super(FakeGithubConnection, self).__init__(driver, connection_name,
946 connection_config)
947 self.connection_name = connection_name
948 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800949 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700950 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700951 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100952 self.merge_failure = False
953 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100954 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800955 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Jesse Keating80730e62017-09-14 15:35:11 -0600956 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200957
958 def getGithubClient(self,
959 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600960 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200961 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700962
Jesse Keating80730e62017-09-14 15:35:11 -0600963 def setZuulWebPort(self, port):
964 self.zuul_web_port = port
965
Jesse Keatinga41566f2017-06-14 18:17:51 -0700966 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700967 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 self.pr_number += 1
969 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100970 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700971 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800972 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700973 return pull_request
974
Jesse Keating71a47ff2017-06-06 11:36:43 -0700975 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
976 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700977 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700978 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700979 if not new_rev:
980 new_rev = random_sha1()
981 name = 'push'
982 data = {
983 'ref': ref,
984 'before': old_rev,
985 'after': new_rev,
986 'repository': {
987 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700988 },
989 'commits': [
990 {
991 'added': added_files,
992 'removed': removed_files,
993 'modified': modified_files
994 }
995 ]
Wayne1a78c612015-06-11 17:14:13 -0700996 }
997 return (name, data)
998
Jesse Keating80730e62017-09-14 15:35:11 -0600999 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001000 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001002 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001003 secret = self.connection_config['webhook_token']
1004 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001005 headers = {'x-github-event': name, 'x-hub-signature': signature}
1006
1007 if use_zuulweb:
1008 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001009 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001010 % (self.zuul_web_port, self.connection_name),
1011 data=payload, headers=headers)
1012 return urllib.request.urlopen(req)
1013 else:
1014 job = self.rpcclient.submitJob(
1015 'github:%s:payload' % self.connection_name,
1016 {'headers': headers, 'body': data})
1017 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001019 def addProject(self, project):
1020 # use the original method here and additionally register it in the
1021 # fake github
1022 super(FakeGithubConnection, self).addProject(project)
1023 self.getGithubClient(project).addProject(project)
1024
Jesse Keating9021a012017-08-29 14:45:27 -07001025 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001026 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001027 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001028 if len(prs) > 1:
1029 raise Exception('Multiple pulls found with head sha: %s' % sha)
1030 pr = prs[0]
1031 return self.getPull(pr.project, pr.number)
1032
Jesse Keatingae4cd272017-01-30 17:10:44 -08001033 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001034 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001035 return pr.reviews
1036
Jesse Keatingae4cd272017-01-30 17:10:44 -08001037 def getRepoPermission(self, project, login):
1038 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001039 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001040 pr_owner, pr_project = pr.project.split('/')
1041 if (pr_owner == owner and proj == pr_project):
1042 if login in pr.writers:
1043 return 'write'
1044 else:
1045 return 'read'
1046
Gregory Haynes4fc12542015-04-22 20:38:06 -07001047 def getGitUrl(self, project):
1048 return os.path.join(self.upstream_root, str(project))
1049
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001050 def real_getGitUrl(self, project):
1051 return super(FakeGithubConnection, self).getGitUrl(project)
1052
Jan Hrubane252a732017-01-03 15:03:09 +01001053 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001054 # record that this got reported
1055 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001056 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001057 pull_request.addComment(message)
1058
Jan Hruban3b415922016-02-03 13:10:22 +01001059 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001060 # record that this got reported
1061 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001062 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001063 if self.merge_failure:
1064 raise Exception('Pull request was not merged')
1065 if self.merge_not_allowed_count > 0:
1066 self.merge_not_allowed_count -= 1
1067 raise MergeFailure('Merge was not successful due to mergeability'
1068 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001069 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001070
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001071 def setCommitStatus(self, project, sha, state, url='', description='',
1072 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001073 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001074 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001075 super(FakeGithubConnection, self).setCommitStatus(
1076 project, sha, state,
1077 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001078
Jan Hruban16ad31f2015-11-07 14:39:07 +01001079 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001080 # record that this got reported
1081 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001082 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001083 pull_request.addLabel(label)
1084
1085 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001086 # record that this got reported
1087 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001088 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001089 pull_request.removeLabel(label)
1090
Gregory Haynes4fc12542015-04-22 20:38:06 -07001091
Clark Boylanb640e052014-04-03 16:41:46 -07001092class BuildHistory(object):
1093 def __init__(self, **kw):
1094 self.__dict__.update(kw)
1095
1096 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001097 return ("<Completed build, result: %s name: %s uuid: %s "
1098 "changes: %s ref: %s>" %
1099 (self.result, self.name, self.uuid,
1100 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001101
1102
Clark Boylanb640e052014-04-03 16:41:46 -07001103class FakeStatsd(threading.Thread):
1104 def __init__(self):
1105 threading.Thread.__init__(self)
1106 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001107 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001108 self.sock.bind(('', 0))
1109 self.port = self.sock.getsockname()[1]
1110 self.wake_read, self.wake_write = os.pipe()
1111 self.stats = []
1112
1113 def run(self):
1114 while True:
1115 poll = select.poll()
1116 poll.register(self.sock, select.POLLIN)
1117 poll.register(self.wake_read, select.POLLIN)
1118 ret = poll.poll()
1119 for (fd, event) in ret:
1120 if fd == self.sock.fileno():
1121 data = self.sock.recvfrom(1024)
1122 if not data:
1123 return
1124 self.stats.append(data[0])
1125 if fd == self.wake_read:
1126 return
1127
1128 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001129 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001130
1131
James E. Blaire1767bc2016-08-02 10:00:27 -07001132class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 log = logging.getLogger("zuul.test")
1134
Paul Belanger174a8272017-03-14 13:20:10 -04001135 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001137 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001139 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001140 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001142 # TODOv3(jeblair): self.node is really "the label of the node
1143 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001144 # keep using it like this, or we may end up exposing more of
1145 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001146 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001147 self.node = None
1148 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001149 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001150 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001151 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001152 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001153 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.wait_condition = threading.Condition()
1155 self.waiting = False
1156 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001157 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001158 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001159 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001160 items = self.parameters['zuul']['items']
1161 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1162 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001163
James E. Blair3158e282016-08-19 09:34:11 -07001164 def __repr__(self):
1165 waiting = ''
1166 if self.waiting:
1167 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001168 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1169 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001170
Clark Boylanb640e052014-04-03 16:41:46 -07001171 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001172 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.wait_condition.acquire()
1174 self.wait_condition.notify()
1175 self.waiting = False
1176 self.log.debug("Build %s released" % self.unique)
1177 self.wait_condition.release()
1178
1179 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001180 """Return whether this build is being held.
1181
1182 :returns: Whether the build is being held.
1183 :rtype: bool
1184 """
1185
Clark Boylanb640e052014-04-03 16:41:46 -07001186 self.wait_condition.acquire()
1187 if self.waiting:
1188 ret = True
1189 else:
1190 ret = False
1191 self.wait_condition.release()
1192 return ret
1193
1194 def _wait(self):
1195 self.wait_condition.acquire()
1196 self.waiting = True
1197 self.log.debug("Build %s waiting" % self.unique)
1198 self.wait_condition.wait()
1199 self.wait_condition.release()
1200
1201 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001202 self.log.debug('Running build %s' % self.unique)
1203
Paul Belanger174a8272017-03-14 13:20:10 -04001204 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.log.debug('Holding build %s' % self.unique)
1206 self._wait()
1207 self.log.debug("Build %s continuing" % self.unique)
1208
James E. Blair412fba82017-01-26 15:00:50 -08001209 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001210 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001211 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001212 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001213 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001214 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001215 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001216
James E. Blaire1767bc2016-08-02 10:00:27 -07001217 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001218
James E. Blaira5dba232016-08-08 15:53:24 -07001219 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001220 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001221 for change in changes:
1222 if self.hasChanges(change):
1223 return True
1224 return False
1225
James E. Blaire7b99a02016-08-05 14:27:34 -07001226 def hasChanges(self, *changes):
1227 """Return whether this build has certain changes in its git repos.
1228
1229 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001230 are expected to be present (in order) in the git repository of
1231 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001232
1233 :returns: Whether the build has the indicated changes.
1234 :rtype: bool
1235
1236 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001237 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001238 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001239 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001240 try:
1241 repo = git.Repo(path)
1242 except NoSuchPathError as e:
1243 self.log.debug('%s' % e)
1244 return False
James E. Blair247cab72017-07-20 16:52:36 -07001245 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001246 commit_message = '%s-1' % change.subject
1247 self.log.debug("Checking if build %s has changes; commit_message "
1248 "%s; repo_messages %s" % (self, commit_message,
1249 repo_messages))
1250 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001251 self.log.debug(" messages do not match")
1252 return False
1253 self.log.debug(" OK")
1254 return True
1255
James E. Blaird8af5422017-05-24 13:59:40 -07001256 def getWorkspaceRepos(self, projects):
1257 """Return workspace git repo objects for the listed projects
1258
1259 :arg list projects: A list of strings, each the canonical name
1260 of a project.
1261
1262 :returns: A dictionary of {name: repo} for every listed
1263 project.
1264 :rtype: dict
1265
1266 """
1267
1268 repos = {}
1269 for project in projects:
1270 path = os.path.join(self.jobdir.src_root, project)
1271 repo = git.Repo(path)
1272 repos[project] = repo
1273 return repos
1274
Clark Boylanb640e052014-04-03 16:41:46 -07001275
James E. Blair107bb252017-10-13 15:53:16 -07001276class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1277 def doMergeChanges(self, merger, items, repo_state):
1278 # Get a merger in order to update the repos involved in this job.
1279 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1280 merger, items, repo_state)
1281 if not commit: # merge conflict
1282 self.recordResult('MERGER_FAILURE')
1283 return commit
1284
1285 def recordResult(self, result):
1286 build = self.executor_server.job_builds[self.job.unique]
1287 self.executor_server.lock.acquire()
1288 self.executor_server.build_history.append(
1289 BuildHistory(name=build.name, result=result, changes=build.changes,
1290 node=build.node, uuid=build.unique,
1291 ref=build.parameters['zuul']['ref'],
1292 parameters=build.parameters, jobdir=build.jobdir,
1293 pipeline=build.parameters['zuul']['pipeline'])
1294 )
1295 self.executor_server.running_builds.remove(build)
1296 del self.executor_server.job_builds[self.job.unique]
1297 self.executor_server.lock.release()
1298
1299 def runPlaybooks(self, args):
1300 build = self.executor_server.job_builds[self.job.unique]
1301 build.jobdir = self.jobdir
1302
1303 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1304 self.recordResult(result)
1305 return result
1306
James E. Blaira86aaf12017-10-15 20:59:50 -07001307 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001308 build = self.executor_server.job_builds[self.job.unique]
1309
1310 if self.executor_server._run_ansible:
1311 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001312 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001313 else:
1314 if playbook.path:
1315 result = build.run()
1316 else:
1317 result = (self.RESULT_NORMAL, 0)
1318 return result
1319
1320 def getHostList(self, args):
1321 self.log.debug("hostlist")
1322 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1323 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001324 if not host['host_vars'].get('ansible_connection'):
1325 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001326
1327 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001328 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001329 host_vars=dict(ansible_connection='local'),
1330 host_keys=[]))
1331 return hosts
1332
1333
Paul Belanger174a8272017-03-14 13:20:10 -04001334class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1335 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001336
Paul Belanger174a8272017-03-14 13:20:10 -04001337 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001338 they will report that they have started but then pause until
1339 released before reporting completion. This attribute may be
1340 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001341 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001342 be explicitly released.
1343
1344 """
James E. Blairfaf81982017-10-10 15:42:26 -07001345
1346 _job_class = RecordingAnsibleJob
1347
James E. Blairf5dbd002015-12-23 15:26:17 -08001348 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001349 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001350 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001351 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001352 self.hold_jobs_in_build = False
1353 self.lock = threading.Lock()
1354 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001355 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001357 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001358
James E. Blaira5dba232016-08-08 15:53:24 -07001359 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001360 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001361
1362 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001363 :arg Change change: The :py:class:`~tests.base.FakeChange`
1364 instance which should cause the job to fail. This job
1365 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001366
1367 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001368 l = self.fail_tests.get(name, [])
1369 l.append(change)
1370 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001371
James E. Blair962220f2016-08-03 11:22:38 -07001372 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001373 """Release a held build.
1374
1375 :arg str regex: A regular expression which, if supplied, will
1376 cause only builds with matching names to be released. If
1377 not supplied, all builds will be released.
1378
1379 """
James E. Blair962220f2016-08-03 11:22:38 -07001380 builds = self.running_builds[:]
1381 self.log.debug("Releasing build %s (%s)" % (regex,
1382 len(self.running_builds)))
1383 for build in builds:
1384 if not regex or re.match(regex, build.name):
1385 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001386 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001387 build.release()
1388 else:
1389 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001390 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001391 self.log.debug("Done releasing builds %s (%s)" %
1392 (regex, len(self.running_builds)))
1393
Paul Belanger174a8272017-03-14 13:20:10 -04001394 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001395 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001398 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001399 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001400 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001401 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001402 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001403
1404 def stopJob(self, job):
1405 self.log.debug("handle stop")
1406 parameters = json.loads(job.arguments)
1407 uuid = parameters['uuid']
1408 for build in self.running_builds:
1409 if build.unique == uuid:
1410 build.aborted = True
1411 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001412 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001413
James E. Blaira002b032017-04-18 10:35:48 -07001414 def stop(self):
1415 for build in self.running_builds:
1416 build.release()
1417 super(RecordingExecutorServer, self).stop()
1418
Joshua Hesketh50c21782016-10-13 21:34:14 +11001419
Clark Boylanb640e052014-04-03 16:41:46 -07001420class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001421 """A Gearman server for use in tests.
1422
1423 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1424 added to the queue but will not be distributed to workers
1425 until released. This attribute may be changed at any time and
1426 will take effect for subsequently enqueued jobs, but
1427 previously held jobs will still need to be explicitly
1428 released.
1429
1430 """
1431
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001432 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001433 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001434 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001435 if use_ssl:
1436 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1437 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1438 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1439 else:
1440 ssl_ca = None
1441 ssl_cert = None
1442 ssl_key = None
1443
1444 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1445 ssl_cert=ssl_cert,
1446 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001447
1448 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001449 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1450 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001451 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001452 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001453 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001454 elif job.name.startswith(b'merger:'):
1455 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001456 else:
1457 job.waiting = False
1458 if job.waiting:
1459 continue
1460 if job.name in connection.functions:
1461 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001462 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001463 connection.related_jobs[job.handle] = job
1464 job.worker_connection = connection
1465 job.running = True
1466 return job
1467 return None
1468
1469 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001470 """Release a held job.
1471
1472 :arg str regex: A regular expression which, if supplied, will
1473 cause only jobs with matching names to be released. If
1474 not supplied, all jobs will be released.
1475 """
Clark Boylanb640e052014-04-03 16:41:46 -07001476 released = False
1477 qlen = (len(self.high_queue) + len(self.normal_queue) +
1478 len(self.low_queue))
1479 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1480 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001481 match = False
1482 if job.name == b'executor:execute':
1483 parameters = json.loads(job.arguments.decode('utf8'))
1484 if not regex or re.match(regex, parameters.get('job')):
1485 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001486 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001487 if not regex:
1488 match = True
1489 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001490 self.log.debug("releasing queued job %s" %
1491 job.unique)
1492 job.waiting = False
1493 released = True
1494 else:
1495 self.log.debug("not releasing queued job %s" %
1496 job.unique)
1497 if released:
1498 self.wakeConnections()
1499 qlen = (len(self.high_queue) + len(self.normal_queue) +
1500 len(self.low_queue))
1501 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1502
1503
1504class FakeSMTP(object):
1505 log = logging.getLogger('zuul.FakeSMTP')
1506
1507 def __init__(self, messages, server, port):
1508 self.server = server
1509 self.port = port
1510 self.messages = messages
1511
1512 def sendmail(self, from_email, to_email, msg):
1513 self.log.info("Sending email from %s, to %s, with msg %s" % (
1514 from_email, to_email, msg))
1515
1516 headers = msg.split('\n\n', 1)[0]
1517 body = msg.split('\n\n', 1)[1]
1518
1519 self.messages.append(dict(
1520 from_email=from_email,
1521 to_email=to_email,
1522 msg=msg,
1523 headers=headers,
1524 body=body,
1525 ))
1526
1527 return True
1528
1529 def quit(self):
1530 return True
1531
1532
James E. Blairdce6cea2016-12-20 16:45:32 -08001533class FakeNodepool(object):
1534 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001535 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001536
1537 log = logging.getLogger("zuul.test.FakeNodepool")
1538
1539 def __init__(self, host, port, chroot):
1540 self.client = kazoo.client.KazooClient(
1541 hosts='%s:%s%s' % (host, port, chroot))
1542 self.client.start()
1543 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001544 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001545 self.thread = threading.Thread(target=self.run)
1546 self.thread.daemon = True
1547 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001548 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001549
1550 def stop(self):
1551 self._running = False
1552 self.thread.join()
1553 self.client.stop()
1554 self.client.close()
1555
1556 def run(self):
1557 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001558 try:
1559 self._run()
1560 except Exception:
1561 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001562 time.sleep(0.1)
1563
1564 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001565 if self.paused:
1566 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001567 for req in self.getNodeRequests():
1568 self.fulfillRequest(req)
1569
1570 def getNodeRequests(self):
1571 try:
1572 reqids = self.client.get_children(self.REQUEST_ROOT)
1573 except kazoo.exceptions.NoNodeError:
1574 return []
1575 reqs = []
1576 for oid in sorted(reqids):
1577 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001578 try:
1579 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001580 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001581 data['_oid'] = oid
1582 reqs.append(data)
1583 except kazoo.exceptions.NoNodeError:
1584 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001585 return reqs
1586
James E. Blaire18d4602017-01-05 11:17:28 -08001587 def getNodes(self):
1588 try:
1589 nodeids = self.client.get_children(self.NODE_ROOT)
1590 except kazoo.exceptions.NoNodeError:
1591 return []
1592 nodes = []
1593 for oid in sorted(nodeids):
1594 path = self.NODE_ROOT + '/' + oid
1595 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001596 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001597 data['_oid'] = oid
1598 try:
1599 lockfiles = self.client.get_children(path + '/lock')
1600 except kazoo.exceptions.NoNodeError:
1601 lockfiles = []
1602 if lockfiles:
1603 data['_lock'] = True
1604 else:
1605 data['_lock'] = False
1606 nodes.append(data)
1607 return nodes
1608
James E. Blaira38c28e2017-01-04 10:33:20 -08001609 def makeNode(self, request_id, node_type):
1610 now = time.time()
1611 path = '/nodepool/nodes/'
1612 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001613 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001614 provider='test-provider',
1615 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001616 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001617 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001618 public_ipv4='127.0.0.1',
1619 private_ipv4=None,
1620 public_ipv6=None,
1621 allocated_to=request_id,
1622 state='ready',
1623 state_time=now,
1624 created_time=now,
1625 updated_time=now,
1626 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001627 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001628 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001629 if 'fakeuser' in node_type:
1630 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001631 if 'windows' in node_type:
1632 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001633 if 'network' in node_type:
1634 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001635
Clint Byrumf322fe22017-05-10 20:53:12 -07001636 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001637 path = self.client.create(path, data,
1638 makepath=True,
1639 sequence=True)
1640 nodeid = path.split("/")[-1]
1641 return nodeid
1642
Krzysztof Klimonda37d54032017-10-25 12:16:47 +02001643 def removeNode(self, node):
1644 path = self.NODE_ROOT + '/' + node["_oid"]
1645 self.client.delete(path, recursive=True)
1646
James E. Blair6ab79e02017-01-06 10:10:17 -08001647 def addFailRequest(self, request):
1648 self.fail_requests.add(request['_oid'])
1649
James E. Blairdce6cea2016-12-20 16:45:32 -08001650 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001651 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001652 return
1653 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001654 oid = request['_oid']
1655 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001656
James E. Blair6ab79e02017-01-06 10:10:17 -08001657 if oid in self.fail_requests:
1658 request['state'] = 'failed'
1659 else:
1660 request['state'] = 'fulfilled'
1661 nodes = []
1662 for node in request['node_types']:
1663 nodeid = self.makeNode(oid, node)
1664 nodes.append(nodeid)
1665 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001666
James E. Blaira38c28e2017-01-04 10:33:20 -08001667 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001668 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001669 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001670 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001671 try:
1672 self.client.set(path, data)
1673 except kazoo.exceptions.NoNodeError:
1674 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001675
1676
James E. Blair498059b2016-12-20 13:50:13 -08001677class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001678 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001679 super(ChrootedKazooFixture, self).__init__()
1680
1681 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1682 if ':' in zk_host:
1683 host, port = zk_host.split(':')
1684 else:
1685 host = zk_host
1686 port = None
1687
1688 self.zookeeper_host = host
1689
1690 if not port:
1691 self.zookeeper_port = 2181
1692 else:
1693 self.zookeeper_port = int(port)
1694
Clark Boylan621ec9a2017-04-07 17:41:33 -07001695 self.test_id = test_id
1696
James E. Blair498059b2016-12-20 13:50:13 -08001697 def _setUp(self):
1698 # Make sure the test chroot paths do not conflict
1699 random_bits = ''.join(random.choice(string.ascii_lowercase +
1700 string.ascii_uppercase)
1701 for x in range(8))
1702
Clark Boylan621ec9a2017-04-07 17:41:33 -07001703 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001704 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1705
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001706 self.addCleanup(self._cleanup)
1707
James E. Blair498059b2016-12-20 13:50:13 -08001708 # Ensure the chroot path exists and clean up any pre-existing znodes.
1709 _tmp_client = kazoo.client.KazooClient(
1710 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1711 _tmp_client.start()
1712
1713 if _tmp_client.exists(self.zookeeper_chroot):
1714 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1715
1716 _tmp_client.ensure_path(self.zookeeper_chroot)
1717 _tmp_client.stop()
1718 _tmp_client.close()
1719
James E. Blair498059b2016-12-20 13:50:13 -08001720 def _cleanup(self):
1721 '''Remove the chroot path.'''
1722 # Need a non-chroot'ed client to remove the chroot path
1723 _tmp_client = kazoo.client.KazooClient(
1724 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1725 _tmp_client.start()
1726 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1727 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001728 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001729
1730
Joshua Heskethd78b4482015-09-14 16:56:34 -06001731class MySQLSchemaFixture(fixtures.Fixture):
1732 def setUp(self):
1733 super(MySQLSchemaFixture, self).setUp()
1734
1735 random_bits = ''.join(random.choice(string.ascii_lowercase +
1736 string.ascii_uppercase)
1737 for x in range(8))
1738 self.name = '%s_%s' % (random_bits, os.getpid())
1739 self.passwd = uuid.uuid4().hex
1740 db = pymysql.connect(host="localhost",
1741 user="openstack_citest",
1742 passwd="openstack_citest",
1743 db="openstack_citest")
1744 cur = db.cursor()
1745 cur.execute("create database %s" % self.name)
1746 cur.execute(
1747 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1748 (self.name, self.name, self.passwd))
1749 cur.execute("flush privileges")
1750
1751 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1752 self.passwd,
1753 self.name)
1754 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1755 self.addCleanup(self.cleanup)
1756
1757 def cleanup(self):
1758 db = pymysql.connect(host="localhost",
1759 user="openstack_citest",
1760 passwd="openstack_citest",
1761 db="openstack_citest")
1762 cur = db.cursor()
1763 cur.execute("drop database %s" % self.name)
1764 cur.execute("drop user '%s'@'localhost'" % self.name)
1765 cur.execute("flush privileges")
1766
1767
Maru Newby3fe5f852015-01-13 04:22:14 +00001768class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001769 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001770 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001771
James E. Blair1c236df2017-02-01 14:07:24 -08001772 def attachLogs(self, *args):
1773 def reader():
1774 self._log_stream.seek(0)
1775 while True:
1776 x = self._log_stream.read(4096)
1777 if not x:
1778 break
1779 yield x.encode('utf8')
1780 content = testtools.content.content_from_reader(
1781 reader,
1782 testtools.content_type.UTF8_TEXT,
1783 False)
1784 self.addDetail('logging', content)
1785
Clark Boylanb640e052014-04-03 16:41:46 -07001786 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001787 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001788 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1789 try:
1790 test_timeout = int(test_timeout)
1791 except ValueError:
1792 # If timeout value is invalid do not set a timeout.
1793 test_timeout = 0
1794 if test_timeout > 0:
1795 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1796
1797 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1798 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1799 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1800 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1801 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1802 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1803 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1804 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1805 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1806 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001807 self._log_stream = StringIO()
1808 self.addOnException(self.attachLogs)
1809 else:
1810 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001811
James E. Blair1c236df2017-02-01 14:07:24 -08001812 handler = logging.StreamHandler(self._log_stream)
1813 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1814 '%(levelname)-8s %(message)s')
1815 handler.setFormatter(formatter)
1816
1817 logger = logging.getLogger()
1818 logger.setLevel(logging.DEBUG)
1819 logger.addHandler(handler)
1820
Clark Boylan3410d532017-04-25 12:35:29 -07001821 # Make sure we don't carry old handlers around in process state
1822 # which slows down test runs
1823 self.addCleanup(logger.removeHandler, handler)
1824 self.addCleanup(handler.close)
1825 self.addCleanup(handler.flush)
1826
James E. Blair1c236df2017-02-01 14:07:24 -08001827 # NOTE(notmorgan): Extract logging overrides for specific
1828 # libraries from the OS_LOG_DEFAULTS env and create loggers
1829 # for each. This is used to limit the output during test runs
1830 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001831 log_defaults_from_env = os.environ.get(
1832 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001833 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001834
James E. Blairdce6cea2016-12-20 16:45:32 -08001835 if log_defaults_from_env:
1836 for default in log_defaults_from_env.split(','):
1837 try:
1838 name, level_str = default.split('=', 1)
1839 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001840 logger = logging.getLogger(name)
1841 logger.setLevel(level)
1842 logger.addHandler(handler)
1843 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001844 except ValueError:
1845 # NOTE(notmorgan): Invalid format of the log default,
1846 # skip and don't try and apply a logger for the
1847 # specified module
1848 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001849
Maru Newby3fe5f852015-01-13 04:22:14 +00001850
1851class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001852 """A test case with a functioning Zuul.
1853
1854 The following class variables are used during test setup and can
1855 be overidden by subclasses but are effectively read-only once a
1856 test method starts running:
1857
1858 :cvar str config_file: This points to the main zuul config file
1859 within the fixtures directory. Subclasses may override this
1860 to obtain a different behavior.
1861
1862 :cvar str tenant_config_file: This is the tenant config file
1863 (which specifies from what git repos the configuration should
1864 be loaded). It defaults to the value specified in
1865 `config_file` but can be overidden by subclasses to obtain a
1866 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001867 configuration. See also the :py:func:`simple_layout`
1868 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001869
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001870 :cvar bool create_project_keys: Indicates whether Zuul should
1871 auto-generate keys for each project, or whether the test
1872 infrastructure should insert dummy keys to save time during
1873 startup. Defaults to False.
1874
James E. Blaire7b99a02016-08-05 14:27:34 -07001875 The following are instance variables that are useful within test
1876 methods:
1877
1878 :ivar FakeGerritConnection fake_<connection>:
1879 A :py:class:`~tests.base.FakeGerritConnection` will be
1880 instantiated for each connection present in the config file
1881 and stored here. For instance, `fake_gerrit` will hold the
1882 FakeGerritConnection object for a connection named `gerrit`.
1883
1884 :ivar FakeGearmanServer gearman_server: An instance of
1885 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1886 server that all of the Zuul components in this test use to
1887 communicate with each other.
1888
Paul Belanger174a8272017-03-14 13:20:10 -04001889 :ivar RecordingExecutorServer executor_server: An instance of
1890 :py:class:`~tests.base.RecordingExecutorServer` which is the
1891 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001892
1893 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1894 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001895 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001896 list upon completion.
1897
1898 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1899 objects representing completed builds. They are appended to
1900 the list in the order they complete.
1901
1902 """
1903
James E. Blair83005782015-12-11 14:46:03 -08001904 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001905 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001906 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001907 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001908
1909 def _startMerger(self):
1910 self.merge_server = zuul.merger.server.MergeServer(self.config,
1911 self.connections)
1912 self.merge_server.start()
1913
Maru Newby3fe5f852015-01-13 04:22:14 +00001914 def setUp(self):
1915 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001916
1917 self.setupZK()
1918
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001919 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001920 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001921 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1922 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001923 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001924 tmp_root = tempfile.mkdtemp(
1925 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001926 self.test_root = os.path.join(tmp_root, "zuul-test")
1927 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001928 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001929 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001930 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001931 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1932 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001933
1934 if os.path.exists(self.test_root):
1935 shutil.rmtree(self.test_root)
1936 os.makedirs(self.test_root)
1937 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001938 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001939 os.makedirs(self.merger_state_root)
1940 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001941
1942 # Make per test copy of Configuration.
1943 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001944 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1945 if not os.path.exists(self.private_key_file):
1946 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1947 shutil.copy(src_private_key_file, self.private_key_file)
1948 shutil.copy('{}.pub'.format(src_private_key_file),
1949 '{}.pub'.format(self.private_key_file))
1950 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001951 self.config.set('scheduler', 'tenant_config',
1952 os.path.join(
1953 FIXTURE_DIR,
1954 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001955 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001956 self.config.set(
1957 'scheduler', 'command_socket',
1958 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001959 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001960 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001961 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001962 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001963 self.config.set(
1964 'executor', 'command_socket',
1965 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001966 self.config.set(
1967 'merger', 'command_socket',
1968 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001969
Clark Boylanb640e052014-04-03 16:41:46 -07001970 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001971 if self.config.has_section('statsd'):
1972 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001973 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001974
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001975 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001976
1977 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001978 self.log.info("Gearman server on port %s" %
1979 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001980 if self.use_ssl:
1981 self.log.info('SSL enabled for gearman')
1982 self.config.set(
1983 'gearman', 'ssl_ca',
1984 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1985 self.config.set(
1986 'gearman', 'ssl_cert',
1987 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1988 self.config.set(
1989 'gearman', 'ssl_key',
1990 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001991
Jesse Keating80730e62017-09-14 15:35:11 -06001992 self.rpcclient = zuul.rpcclient.RPCClient(
1993 self.config.get('gearman', 'server'),
1994 self.gearman_server.port,
1995 get_default(self.config, 'gearman', 'ssl_key'),
1996 get_default(self.config, 'gearman', 'ssl_cert'),
1997 get_default(self.config, 'gearman', 'ssl_ca'))
1998
James E. Blaire511d2f2016-12-08 15:22:26 -08001999 gerritsource.GerritSource.replication_timeout = 1.5
2000 gerritsource.GerritSource.replication_retry_interval = 0.5
2001 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002002
Joshua Hesketh352264b2015-08-11 23:42:08 +10002003 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002004 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002005
Jan Hruban6b71aff2015-10-22 16:58:08 +02002006 self.event_queues = [
2007 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002008 self.sched.trigger_event_queue,
2009 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002010 ]
2011
James E. Blairfef78942016-03-11 16:28:56 -08002012 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002013 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002014
Paul Belanger174a8272017-03-14 13:20:10 -04002015 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002016 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002017 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002018 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002019 _test_root=self.test_root,
2020 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002021 self.executor_server.start()
2022 self.history = self.executor_server.build_history
2023 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002024
Paul Belanger174a8272017-03-14 13:20:10 -04002025 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002026 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002027 self.merge_client = zuul.merger.client.MergeClient(
2028 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002029 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002030 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002031 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002032 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002033
James E. Blair0d5a36e2017-02-21 10:53:44 -05002034 self.fake_nodepool = FakeNodepool(
2035 self.zk_chroot_fixture.zookeeper_host,
2036 self.zk_chroot_fixture.zookeeper_port,
2037 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002038
Paul Belanger174a8272017-03-14 13:20:10 -04002039 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002040 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002041 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002042 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002043
Clark Boylanb640e052014-04-03 16:41:46 -07002044 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002045 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002046 # Cleanups are run in reverse order
2047 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002048 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002049 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002050
James E. Blairb9c0d772017-03-03 14:34:49 -08002051 self.sched.reconfigure(self.config)
2052 self.sched.resume()
2053
Tobias Henkel7df274b2017-05-26 17:41:11 +02002054 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002055 # Set up gerrit related fakes
2056 # Set a changes database so multiple FakeGerrit's can report back to
2057 # a virtual canonical database given by the configured hostname
2058 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002059 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002060
2061 def getGerritConnection(driver, name, config):
2062 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2063 con = FakeGerritConnection(driver, name, config,
2064 changes_db=db,
2065 upstream_root=self.upstream_root)
2066 self.event_queues.append(con.event_queue)
2067 setattr(self, 'fake_' + name, con)
2068 return con
2069
2070 self.useFixture(fixtures.MonkeyPatch(
2071 'zuul.driver.gerrit.GerritDriver.getConnection',
2072 getGerritConnection))
2073
Gregory Haynes4fc12542015-04-22 20:38:06 -07002074 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002075 server = config.get('server', 'github.com')
2076 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002077 con = FakeGithubConnection(driver, name, config,
Jesse Keating80730e62017-09-14 15:35:11 -06002078 self.rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -08002079 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002080 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002081 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002082 setattr(self, 'fake_' + name, con)
2083 return con
2084
2085 self.useFixture(fixtures.MonkeyPatch(
2086 'zuul.driver.github.GithubDriver.getConnection',
2087 getGithubConnection))
2088
James E. Blaire511d2f2016-12-08 15:22:26 -08002089 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002090 # TODO(jhesketh): This should come from lib.connections for better
2091 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002092 # Register connections from the config
2093 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002094
Joshua Hesketh352264b2015-08-11 23:42:08 +10002095 def FakeSMTPFactory(*args, **kw):
2096 args = [self.smtp_messages] + list(args)
2097 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002098
Joshua Hesketh352264b2015-08-11 23:42:08 +10002099 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002100
James E. Blaire511d2f2016-12-08 15:22:26 -08002101 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002102 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002103 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002104
James E. Blair83005782015-12-11 14:46:03 -08002105 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002106 # This creates the per-test configuration object. It can be
2107 # overriden by subclasses, but should not need to be since it
2108 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002109 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002110 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002111
James E. Blair39840362017-06-23 20:34:02 +01002112 sections = ['zuul', 'scheduler', 'executor', 'merger']
2113 for section in sections:
2114 if not self.config.has_section(section):
2115 self.config.add_section(section)
2116
James E. Blair06cc3922017-04-19 10:08:10 -07002117 if not self.setupSimpleLayout():
2118 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002119 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002120 self.tenant_config_file)
2121 git_path = os.path.join(
2122 os.path.dirname(
2123 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2124 'git')
2125 if os.path.exists(git_path):
2126 for reponame in os.listdir(git_path):
2127 project = reponame.replace('_', '/')
2128 self.copyDirToRepo(project,
2129 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002130 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002131 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002132 self.setupAllProjectKeys()
2133
James E. Blair06cc3922017-04-19 10:08:10 -07002134 def setupSimpleLayout(self):
2135 # If the test method has been decorated with a simple_layout,
2136 # use that instead of the class tenant_config_file. Set up a
2137 # single config-project with the specified layout, and
2138 # initialize repos for all of the 'project' entries which
2139 # appear in the layout.
2140 test_name = self.id().split('.')[-1]
2141 test = getattr(self, test_name)
2142 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002143 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002144 else:
2145 return False
2146
James E. Blairb70e55a2017-04-19 12:57:02 -07002147 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002148 path = os.path.join(FIXTURE_DIR, path)
2149 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002150 data = f.read()
2151 layout = yaml.safe_load(data)
2152 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002153 untrusted_projects = []
2154 for item in layout:
2155 if 'project' in item:
2156 name = item['project']['name']
2157 untrusted_projects.append(name)
2158 self.init_repo(name)
2159 self.addCommitToRepo(name, 'initial commit',
2160 files={'README': ''},
2161 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002162 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002163 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002164 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002165 for fn in zuul.configloader.as_list(
2166 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002167 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002168 for fn in zuul.configloader.as_list(
2169 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002170 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002171
2172 root = os.path.join(self.test_root, "config")
2173 if not os.path.exists(root):
2174 os.makedirs(root)
2175 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2176 config = [{'tenant':
2177 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002178 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002179 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002180 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002181 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002182 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002183 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002184 os.path.join(FIXTURE_DIR, f.name))
2185
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002186 self.init_repo('org/common-config')
2187 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002188 files, branch='master', tag='init')
2189
2190 return True
2191
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002192 def setupAllProjectKeys(self):
2193 if self.create_project_keys:
2194 return
2195
James E. Blair39840362017-06-23 20:34:02 +01002196 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002197 with open(os.path.join(FIXTURE_DIR, path)) as f:
2198 tenant_config = yaml.safe_load(f.read())
2199 for tenant in tenant_config:
2200 sources = tenant['tenant']['source']
2201 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002202 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002203 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002204 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002205 self.setupProjectKeys(source, project)
2206
2207 def setupProjectKeys(self, source, project):
2208 # Make sure we set up an RSA key for the project so that we
2209 # don't spend time generating one:
2210
James E. Blair6459db12017-06-29 14:57:20 -07002211 if isinstance(project, dict):
2212 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002213 key_root = os.path.join(self.state_root, 'keys')
2214 if not os.path.isdir(key_root):
2215 os.mkdir(key_root, 0o700)
2216 private_key_file = os.path.join(key_root, source, project + '.pem')
2217 private_key_dir = os.path.dirname(private_key_file)
2218 self.log.debug("Installing test keys for project %s at %s" % (
2219 project, private_key_file))
2220 if not os.path.isdir(private_key_dir):
2221 os.makedirs(private_key_dir)
2222 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2223 with open(private_key_file, 'w') as o:
2224 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002225
James E. Blair498059b2016-12-20 13:50:13 -08002226 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002227 self.zk_chroot_fixture = self.useFixture(
2228 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002229 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002230 self.zk_chroot_fixture.zookeeper_host,
2231 self.zk_chroot_fixture.zookeeper_port,
2232 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002233
James E. Blair96c6bf82016-01-15 16:20:40 -08002234 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002235 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002236
2237 files = {}
2238 for (dirpath, dirnames, filenames) in os.walk(source_path):
2239 for filename in filenames:
2240 test_tree_filepath = os.path.join(dirpath, filename)
2241 common_path = os.path.commonprefix([test_tree_filepath,
2242 source_path])
2243 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2244 with open(test_tree_filepath, 'r') as f:
2245 content = f.read()
2246 files[relative_filepath] = content
2247 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002248 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002249
James E. Blaire18d4602017-01-05 11:17:28 -08002250 def assertNodepoolState(self):
2251 # Make sure that there are no pending requests
2252
2253 requests = self.fake_nodepool.getNodeRequests()
2254 self.assertEqual(len(requests), 0)
2255
2256 nodes = self.fake_nodepool.getNodes()
2257 for node in nodes:
2258 self.assertFalse(node['_lock'], "Node %s is locked" %
2259 (node['_oid'],))
2260
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002261 def assertNoGeneratedKeys(self):
2262 # Make sure that Zuul did not generate any project keys
2263 # (unless it was supposed to).
2264
2265 if self.create_project_keys:
2266 return
2267
2268 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2269 test_key = i.read()
2270
2271 key_root = os.path.join(self.state_root, 'keys')
2272 for root, dirname, files in os.walk(key_root):
2273 for fn in files:
2274 with open(os.path.join(root, fn)) as f:
2275 self.assertEqual(test_key, f.read())
2276
Clark Boylanb640e052014-04-03 16:41:46 -07002277 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002278 self.log.debug("Assert final state")
2279 # Make sure no jobs are running
2280 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002281 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002282 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002283 gc.collect()
2284 for obj in gc.get_objects():
2285 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002286 self.log.debug("Leaked git repo object: 0x%x %s" %
2287 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002288 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002289 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002290 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002291 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002292 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002293 for tenant in self.sched.abide.tenants.values():
2294 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002295 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002296 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002297
2298 def shutdown(self):
2299 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002300 self.executor_server.hold_jobs_in_build = False
2301 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002302 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002303 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002304 if self.merge_server:
2305 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002306 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002307 self.sched.stop()
2308 self.sched.join()
2309 self.statsd.stop()
2310 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002311 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002312 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002313 self.fake_nodepool.stop()
2314 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002315 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002316 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002317 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002318 # Further the pydevd threads also need to be whitelisted so debugging
2319 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002320 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002321 'pydevd.CommandThread',
2322 'pydevd.Reader',
2323 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002324 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002325 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002326 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002327 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002328 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002329 log_str = ""
2330 for thread_id, stack_frame in sys._current_frames().items():
2331 log_str += "Thread: %s\n" % thread_id
2332 log_str += "".join(traceback.format_stack(stack_frame))
2333 self.log.debug(log_str)
2334 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002335
James E. Blaira002b032017-04-18 10:35:48 -07002336 def assertCleanShutdown(self):
2337 pass
2338
James E. Blairc4ba97a2017-04-19 16:26:24 -07002339 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002340 parts = project.split('/')
2341 path = os.path.join(self.upstream_root, *parts[:-1])
2342 if not os.path.exists(path):
2343 os.makedirs(path)
2344 path = os.path.join(self.upstream_root, project)
2345 repo = git.Repo.init(path)
2346
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002347 with repo.config_writer() as config_writer:
2348 config_writer.set_value('user', 'email', 'user@example.com')
2349 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002350
Clark Boylanb640e052014-04-03 16:41:46 -07002351 repo.index.commit('initial commit')
2352 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002353 if tag:
2354 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002355
James E. Blair97d902e2014-08-21 13:25:56 -07002356 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002357 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002358 repo.git.clean('-x', '-f', '-d')
2359
James E. Blair97d902e2014-08-21 13:25:56 -07002360 def create_branch(self, project, branch):
2361 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002362 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002363 fn = os.path.join(path, 'README')
2364
2365 branch_head = repo.create_head(branch)
2366 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002367 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002368 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002369 f.close()
2370 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002371 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002372
James E. Blair97d902e2014-08-21 13:25:56 -07002373 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002374 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002375 repo.git.clean('-x', '-f', '-d')
2376
James E. Blairda5bb7e2018-01-22 16:12:17 -08002377 def delete_branch(self, project, branch):
2378 path = os.path.join(self.upstream_root, project)
2379 repo = git.Repo(path)
2380 repo.head.reference = repo.heads['master']
2381 zuul.merger.merger.reset_repo_to_head(repo)
2382 repo.delete_head(repo.heads[branch], force=True)
2383
Sachi King9f16d522016-03-16 12:20:45 +11002384 def create_commit(self, project):
2385 path = os.path.join(self.upstream_root, project)
2386 repo = git.Repo(path)
2387 repo.head.reference = repo.heads['master']
2388 file_name = os.path.join(path, 'README')
2389 with open(file_name, 'a') as f:
2390 f.write('creating fake commit\n')
2391 repo.index.add([file_name])
2392 commit = repo.index.commit('Creating a fake commit')
2393 return commit.hexsha
2394
James E. Blairf4a5f022017-04-18 14:01:10 -07002395 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002396 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002397 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002398 while len(self.builds):
2399 self.release(self.builds[0])
2400 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002401 i += 1
2402 if count is not None and i >= count:
2403 break
James E. Blairb8c16472015-05-05 14:55:26 -07002404
James E. Blairdf25ddc2017-07-08 07:57:09 -07002405 def getSortedBuilds(self):
2406 "Return the list of currently running builds sorted by name"
2407
2408 return sorted(self.builds, key=lambda x: x.name)
2409
Clark Boylanb640e052014-04-03 16:41:46 -07002410 def release(self, job):
2411 if isinstance(job, FakeBuild):
2412 job.release()
2413 else:
2414 job.waiting = False
2415 self.log.debug("Queued job %s released" % job.unique)
2416 self.gearman_server.wakeConnections()
2417
2418 def getParameter(self, job, name):
2419 if isinstance(job, FakeBuild):
2420 return job.parameters[name]
2421 else:
2422 parameters = json.loads(job.arguments)
2423 return parameters[name]
2424
Clark Boylanb640e052014-04-03 16:41:46 -07002425 def haveAllBuildsReported(self):
2426 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002427 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002428 return False
2429 # Find out if every build that the worker has completed has been
2430 # reported back to Zuul. If it hasn't then that means a Gearman
2431 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002432 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002433 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002434 if not zbuild:
2435 # It has already been reported
2436 continue
2437 # It hasn't been reported yet.
2438 return False
2439 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002440 worker = self.executor_server.executor_worker
2441 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002442 if connection.state == 'GRAB_WAIT':
2443 return False
2444 return True
2445
2446 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002447 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002448 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002449 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002450 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002451 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002452 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002453 for j in conn.related_jobs.values():
2454 if j.unique == build.uuid:
2455 client_job = j
2456 break
2457 if not client_job:
2458 self.log.debug("%s is not known to the gearman client" %
2459 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002460 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002461 if not client_job.handle:
2462 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002463 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002464 server_job = self.gearman_server.jobs.get(client_job.handle)
2465 if not server_job:
2466 self.log.debug("%s is not known to the gearman server" %
2467 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002468 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002469 if not hasattr(server_job, 'waiting'):
2470 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002471 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002472 if server_job.waiting:
2473 continue
James E. Blair17302972016-08-10 16:11:42 -07002474 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002475 self.log.debug("%s has not reported start" % build)
2476 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002477 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002478 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002479 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002480 if worker_build:
2481 if worker_build.isWaiting():
2482 continue
2483 else:
2484 self.log.debug("%s is running" % worker_build)
2485 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002486 else:
James E. Blair962220f2016-08-03 11:22:38 -07002487 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002488 return False
James E. Blaira002b032017-04-18 10:35:48 -07002489 for (build_uuid, job_worker) in \
2490 self.executor_server.job_workers.items():
2491 if build_uuid not in seen_builds:
2492 self.log.debug("%s is not finalized" % build_uuid)
2493 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002494 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002495
James E. Blairdce6cea2016-12-20 16:45:32 -08002496 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002497 if self.fake_nodepool.paused:
2498 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002499 if self.sched.nodepool.requests:
2500 return False
2501 return True
2502
James E. Blaira615c362017-10-02 17:34:42 -07002503 def areAllMergeJobsWaiting(self):
2504 for client_job in list(self.merge_client.jobs):
2505 if not client_job.handle:
2506 self.log.debug("%s has no handle" % client_job)
2507 return False
2508 server_job = self.gearman_server.jobs.get(client_job.handle)
2509 if not server_job:
2510 self.log.debug("%s is not known to the gearman server" %
2511 client_job)
2512 return False
2513 if not hasattr(server_job, 'waiting'):
2514 self.log.debug("%s is being enqueued" % server_job)
2515 return False
2516 if server_job.waiting:
2517 self.log.debug("%s is waiting" % server_job)
2518 continue
2519 self.log.debug("%s is not waiting" % server_job)
2520 return False
2521 return True
2522
Jan Hruban6b71aff2015-10-22 16:58:08 +02002523 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002524 for event_queue in self.event_queues:
2525 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002526
2527 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002528 for event_queue in self.event_queues:
2529 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002530
Clark Boylanb640e052014-04-03 16:41:46 -07002531 def waitUntilSettled(self):
2532 self.log.debug("Waiting until settled...")
2533 start = time.time()
2534 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002535 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002536 self.log.error("Timeout waiting for Zuul to settle")
2537 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002538 for event_queue in self.event_queues:
2539 self.log.error(" %s: %s" %
2540 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002541 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002542 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002543 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002544 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002545 self.log.error("All requests completed: %s" %
2546 (self.areAllNodeRequestsComplete(),))
2547 self.log.error("Merge client jobs: %s" %
2548 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002549 raise Exception("Timeout waiting for Zuul to settle")
2550 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002551
Paul Belanger174a8272017-03-14 13:20:10 -04002552 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002553 # have all build states propogated to zuul?
2554 if self.haveAllBuildsReported():
2555 # Join ensures that the queue is empty _and_ events have been
2556 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002557 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002558 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002559 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002560 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002561 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002562 self.areAllNodeRequestsComplete() and
2563 all(self.eventQueuesEmpty())):
2564 # The queue empty check is placed at the end to
2565 # ensure that if a component adds an event between
2566 # when locked the run handler and checked that the
2567 # components were stable, we don't erroneously
2568 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002569 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002570 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002571 self.log.debug("...settled.")
2572 return
2573 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002574 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002575 self.sched.wake_event.wait(0.1)
2576
2577 def countJobResults(self, jobs, result):
2578 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002579 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002580
Monty Taylor0d926122017-05-24 08:07:56 -05002581 def getBuildByName(self, name):
2582 for build in self.builds:
2583 if build.name == name:
2584 return build
2585 raise Exception("Unable to find build %s" % name)
2586
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002587 def assertJobNotInHistory(self, name, project=None):
2588 for job in self.history:
2589 if (project is None or
2590 job.parameters['zuul']['project']['name'] == project):
2591 self.assertNotEqual(job.name, name,
2592 'Job %s found in history' % name)
2593
James E. Blair96c6bf82016-01-15 16:20:40 -08002594 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002595 for job in self.history:
2596 if (job.name == name and
2597 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002598 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002599 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002600 raise Exception("Unable to find job %s in history" % name)
2601
2602 def assertEmptyQueues(self):
2603 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002604 for tenant in self.sched.abide.tenants.values():
2605 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002606 for pipeline_queue in pipeline.queues:
2607 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002608 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002609 pipeline.name, pipeline_queue.name,
2610 pipeline_queue.queue))
2611 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002612 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002613
2614 def assertReportedStat(self, key, value=None, kind=None):
2615 start = time.time()
2616 while time.time() < (start + 5):
2617 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002618 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002619 if key == k:
2620 if value is None and kind is None:
2621 return
2622 elif value:
2623 if value == v:
2624 return
2625 elif kind:
2626 if v.endswith('|' + kind):
2627 return
2628 time.sleep(0.1)
2629
Clark Boylanb640e052014-04-03 16:41:46 -07002630 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002631
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002632 def assertBuilds(self, builds):
2633 """Assert that the running builds are as described.
2634
2635 The list of running builds is examined and must match exactly
2636 the list of builds described by the input.
2637
2638 :arg list builds: A list of dictionaries. Each item in the
2639 list must match the corresponding build in the build
2640 history, and each element of the dictionary must match the
2641 corresponding attribute of the build.
2642
2643 """
James E. Blair3158e282016-08-19 09:34:11 -07002644 try:
2645 self.assertEqual(len(self.builds), len(builds))
2646 for i, d in enumerate(builds):
2647 for k, v in d.items():
2648 self.assertEqual(
2649 getattr(self.builds[i], k), v,
2650 "Element %i in builds does not match" % (i,))
2651 except Exception:
2652 for build in self.builds:
2653 self.log.error("Running build: %s" % build)
2654 else:
2655 self.log.error("No running builds")
2656 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002657
James E. Blairb536ecc2016-08-31 10:11:42 -07002658 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002659 """Assert that the completed builds are as described.
2660
2661 The list of completed builds is examined and must match
2662 exactly the list of builds described by the input.
2663
2664 :arg list history: A list of dictionaries. Each item in the
2665 list must match the corresponding build in the build
2666 history, and each element of the dictionary must match the
2667 corresponding attribute of the build.
2668
James E. Blairb536ecc2016-08-31 10:11:42 -07002669 :arg bool ordered: If true, the history must match the order
2670 supplied, if false, the builds are permitted to have
2671 arrived in any order.
2672
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002673 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002674 def matches(history_item, item):
2675 for k, v in item.items():
2676 if getattr(history_item, k) != v:
2677 return False
2678 return True
James E. Blair3158e282016-08-19 09:34:11 -07002679 try:
2680 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002681 if ordered:
2682 for i, d in enumerate(history):
2683 if not matches(self.history[i], d):
2684 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002685 "Element %i in history does not match %s" %
2686 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002687 else:
2688 unseen = self.history[:]
2689 for i, d in enumerate(history):
2690 found = False
2691 for unseen_item in unseen:
2692 if matches(unseen_item, d):
2693 found = True
2694 unseen.remove(unseen_item)
2695 break
2696 if not found:
2697 raise Exception("No match found for element %i "
2698 "in history" % (i,))
2699 if unseen:
2700 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002701 except Exception:
2702 for build in self.history:
2703 self.log.error("Completed build: %s" % build)
2704 else:
2705 self.log.error("No completed builds")
2706 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002707
James E. Blair6ac368c2016-12-22 18:07:20 -08002708 def printHistory(self):
2709 """Log the build history.
2710
2711 This can be useful during tests to summarize what jobs have
2712 completed.
2713
2714 """
2715 self.log.debug("Build history:")
2716 for build in self.history:
2717 self.log.debug(build)
2718
James E. Blair59fdbac2015-12-07 17:08:06 -08002719 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002720 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2721
James E. Blair9ea70072017-04-19 16:05:30 -07002722 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002723 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002724 if not os.path.exists(root):
2725 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002726 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2727 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002728- tenant:
2729 name: openstack
2730 source:
2731 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002732 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002733 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002734 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002735 - org/project
2736 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002737 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002738 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002739 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002740 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002741 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002742
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002743 def addTagToRepo(self, project, name, sha):
2744 path = os.path.join(self.upstream_root, project)
2745 repo = git.Repo(path)
2746 repo.git.tag(name, sha)
2747
2748 def delTagFromRepo(self, project, name):
2749 path = os.path.join(self.upstream_root, project)
2750 repo = git.Repo(path)
2751 repo.git.tag('-d', name)
2752
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002753 def addCommitToRepo(self, project, message, files,
2754 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002755 path = os.path.join(self.upstream_root, project)
2756 repo = git.Repo(path)
2757 repo.head.reference = branch
2758 zuul.merger.merger.reset_repo_to_head(repo)
2759 for fn, content in files.items():
2760 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002761 try:
2762 os.makedirs(os.path.dirname(fn))
2763 except OSError:
2764 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002765 with open(fn, 'w') as f:
2766 f.write(content)
2767 repo.index.add([fn])
2768 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002769 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002770 repo.heads[branch].commit = commit
2771 repo.head.reference = branch
2772 repo.git.clean('-x', '-f', '-d')
2773 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002774 if tag:
2775 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002776 return before
2777
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002778 def commitConfigUpdate(self, project_name, source_name):
2779 """Commit an update to zuul.yaml
2780
2781 This overwrites the zuul.yaml in the specificed project with
2782 the contents specified.
2783
2784 :arg str project_name: The name of the project containing
2785 zuul.yaml (e.g., common-config)
2786
2787 :arg str source_name: The path to the file (underneath the
2788 test fixture directory) whose contents should be used to
2789 replace zuul.yaml.
2790 """
2791
2792 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002793 files = {}
2794 with open(source_path, 'r') as f:
2795 data = f.read()
2796 layout = yaml.safe_load(data)
2797 files['zuul.yaml'] = data
2798 for item in layout:
2799 if 'job' in item:
2800 jobname = item['job']['name']
2801 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002802 before = self.addCommitToRepo(
2803 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002804 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002805 return before
2806
Clint Byrum627ba362017-08-14 13:20:40 -07002807 def newTenantConfig(self, source_name):
2808 """ Use this to update the tenant config file in tests
2809
2810 This will update self.tenant_config_file to point to a temporary file
2811 for the duration of this particular test. The content of that file will
2812 be taken from FIXTURE_DIR/source_name
2813
2814 After the test the original value of self.tenant_config_file will be
2815 restored.
2816
2817 :arg str source_name: The path of the file under
2818 FIXTURE_DIR that will be used to populate the new tenant
2819 config file.
2820 """
2821 source_path = os.path.join(FIXTURE_DIR, source_name)
2822 orig_tenant_config_file = self.tenant_config_file
2823 with tempfile.NamedTemporaryFile(
2824 delete=False, mode='wb') as new_tenant_config:
2825 self.tenant_config_file = new_tenant_config.name
2826 with open(source_path, mode='rb') as source_tenant_config:
2827 new_tenant_config.write(source_tenant_config.read())
2828 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2829 self.setupAllProjectKeys()
2830 self.log.debug(
2831 'tenant_config_file = {}'.format(self.tenant_config_file))
2832
2833 def _restoreTenantConfig():
2834 self.log.debug(
2835 'restoring tenant_config_file = {}'.format(
2836 orig_tenant_config_file))
2837 os.unlink(self.tenant_config_file)
2838 self.tenant_config_file = orig_tenant_config_file
2839 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2840 self.addCleanup(_restoreTenantConfig)
2841
James E. Blair7fc8daa2016-08-08 15:37:15 -07002842 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002843
James E. Blair7fc8daa2016-08-08 15:37:15 -07002844 """Inject a Fake (Gerrit) event.
2845
2846 This method accepts a JSON-encoded event and simulates Zuul
2847 having received it from Gerrit. It could (and should)
2848 eventually apply to any connection type, but is currently only
2849 used with Gerrit connections. The name of the connection is
2850 used to look up the corresponding server, and the event is
2851 simulated as having been received by all Zuul connections
2852 attached to that server. So if two Gerrit connections in Zuul
2853 are connected to the same Gerrit server, and you invoke this
2854 method specifying the name of one of them, the event will be
2855 received by both.
2856
2857 .. note::
2858
2859 "self.fake_gerrit.addEvent" calls should be migrated to
2860 this method.
2861
2862 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002863 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002864 :arg str event: The JSON-encoded event.
2865
2866 """
2867 specified_conn = self.connections.connections[connection]
2868 for conn in self.connections.connections.values():
2869 if (isinstance(conn, specified_conn.__class__) and
2870 specified_conn.server == conn.server):
2871 conn.addEvent(event)
2872
James E. Blaird8af5422017-05-24 13:59:40 -07002873 def getUpstreamRepos(self, projects):
2874 """Return upstream git repo objects for the listed projects
2875
2876 :arg list projects: A list of strings, each the canonical name
2877 of a project.
2878
2879 :returns: A dictionary of {name: repo} for every listed
2880 project.
2881 :rtype: dict
2882
2883 """
2884
2885 repos = {}
2886 for project in projects:
2887 # FIXME(jeblair): the upstream root does not yet have a
2888 # hostname component; that needs to be added, and this
2889 # line removed:
2890 tmp_project_name = '/'.join(project.split('/')[1:])
2891 path = os.path.join(self.upstream_root, tmp_project_name)
2892 repo = git.Repo(path)
2893 repos[project] = repo
2894 return repos
2895
James E. Blair3f876d52016-07-22 13:07:14 -07002896
2897class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002898 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002899 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002900
Jamie Lennox7655b552017-03-17 12:33:38 +11002901 @contextmanager
2902 def jobLog(self, build):
2903 """Print job logs on assertion errors
2904
2905 This method is a context manager which, if it encounters an
2906 ecxeption, adds the build log to the debug output.
2907
2908 :arg Build build: The build that's being asserted.
2909 """
2910 try:
2911 yield
2912 except Exception:
2913 path = os.path.join(self.test_root, build.uuid,
2914 'work', 'logs', 'job-output.txt')
2915 with open(path) as f:
2916 self.log.debug(f.read())
2917 raise
2918
Joshua Heskethd78b4482015-09-14 16:56:34 -06002919
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002920class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002921 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002922 use_ssl = True
2923
2924
Joshua Heskethd78b4482015-09-14 16:56:34 -06002925class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002926 def setup_config(self):
2927 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002928 for section_name in self.config.sections():
2929 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2930 section_name, re.I)
2931 if not con_match:
2932 continue
2933
2934 if self.config.get(section_name, 'driver') == 'sql':
2935 f = MySQLSchemaFixture()
2936 self.useFixture(f)
2937 if (self.config.get(section_name, 'dburi') ==
2938 '$MYSQL_FIXTURE_DBURI$'):
2939 self.config.set(section_name, 'dburi', f.dburi)