blob: be8c17bfd4df3ddb45e9ce821f7bcc4c3487ea46 [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
James E. Blairc6d48652018-02-14 14:20:13 -0800532 def getFakeBranchDeletedEvent(self, project, branch):
533 oldrev = '4abd38457c2da2a72d4d030219ab180ecdb04bf0'
534 newrev = 40 * '0'
535
536 event = {
537 "type": "ref-updated",
538 "submitter": {
539 "name": "User Name",
540 },
541 "refUpdate": {
542 "oldRev": oldrev,
543 "newRev": newrev,
544 "refName": 'refs/heads/' + branch,
545 "project": project,
546 }
547 }
548 return event
549
Clark Boylanb640e052014-04-03 16:41:46 -0700550 def review(self, project, changeid, message, action):
551 number, ps = changeid.split(',')
552 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000553
554 # Add the approval back onto the change (ie simulate what gerrit would
555 # do).
556 # Usually when zuul leaves a review it'll create a feedback loop where
557 # zuul's review enters another gerrit event (which is then picked up by
558 # zuul). However, we can't mimic this behaviour (by adding this
559 # approval event into the queue) as it stops jobs from checking what
560 # happens before this event is triggered. If a job needs to see what
561 # happens they can add their own verified event into the queue.
562 # Nevertheless, we can update change with the new review in gerrit.
563
James E. Blair8b5408c2016-08-08 15:37:46 -0700564 for cat in action.keys():
565 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000566 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000567
Clark Boylanb640e052014-04-03 16:41:46 -0700568 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000569
Clark Boylanb640e052014-04-03 16:41:46 -0700570 if 'submit' in action:
571 change.setMerged()
572 if message:
573 change.setReported()
574
575 def query(self, number):
576 change = self.changes.get(int(number))
577 if change:
578 return change.query()
579 return {}
580
James E. Blair0e4c7912018-01-02 14:20:17 -0800581 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800582 if query.startswith('change:'):
583 # Query a specific changeid
584 changeid = query[len('change:'):]
585 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800586 if (change.data['id'] == changeid or
587 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700588 elif query.startswith('message:'):
589 # Query the content of a commit message
590 msg = query[len('message:'):].strip()
591 l = [change.query() for change in self.changes.values()
592 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800593 else:
594 # Query all open changes
595 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700596 return l
James E. Blairc494d542014-08-06 09:23:52 -0700597
James E. Blair0e4c7912018-01-02 14:20:17 -0800598 def simpleQuery(self, query):
599 self.log.debug("simpleQuery: %s" % query)
600 self.queries.append(query)
601 results = []
602 if query.startswith('(') and 'OR' in query:
603 query = query[1:-2]
604 for q in query.split(' OR '):
605 for r in self._simpleQuery(q):
606 if r not in results:
607 results.append(r)
608 else:
609 results = self._simpleQuery(query)
610 return results
611
Joshua Hesketh352264b2015-08-11 23:42:08 +1000612 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700613 pass
614
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200615 def _uploadPack(self, project):
616 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
617 'multi_ack thin-pack side-band side-band-64k ofs-delta '
618 'shallow no-progress include-tag multi_ack_detailed no-done\n')
619 path = os.path.join(self.upstream_root, project.name)
620 repo = git.Repo(path)
621 for ref in repo.refs:
622 r = ref.object.hexsha + ' ' + ref.path + '\n'
623 ret += '%04x%s' % (len(r) + 4, r)
624 ret += '0000'
625 return ret
626
Joshua Hesketh352264b2015-08-11 23:42:08 +1000627 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800628 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000629
Clark Boylanb640e052014-04-03 16:41:46 -0700630
Gregory Haynes4fc12542015-04-22 20:38:06 -0700631class GithubChangeReference(git.Reference):
632 _common_path_default = "refs/pull"
633 _points_to_commits_only = True
634
635
636class FakeGithubPullRequest(object):
637
638 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800639 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700640 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700641 """Creates a new PR with several commits.
642 Sends an event about opened PR."""
643 self.github = github
644 self.source = github
645 self.number = number
646 self.project = project
647 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100648 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700649 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100650 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700651 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100652 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700653 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100654 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100655 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800656 self.reviews = []
657 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700658 self.updated_at = None
659 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100660 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100661 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700662 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800663 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700664 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100665 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700666 self._updateTimeStamp()
667
Jan Hruban570d01c2016-03-10 21:51:32 +0100668 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700669 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100670 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700671 self._updateTimeStamp()
672
Jan Hruban570d01c2016-03-10 21:51:32 +0100673 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700674 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100675 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700676 self._updateTimeStamp()
677
678 def getPullRequestOpenedEvent(self):
679 return self._getPullRequestEvent('opened')
680
681 def getPullRequestSynchronizeEvent(self):
682 return self._getPullRequestEvent('synchronize')
683
684 def getPullRequestReopenedEvent(self):
685 return self._getPullRequestEvent('reopened')
686
687 def getPullRequestClosedEvent(self):
688 return self._getPullRequestEvent('closed')
689
Jesse Keatinga41566f2017-06-14 18:17:51 -0700690 def getPullRequestEditedEvent(self):
691 return self._getPullRequestEvent('edited')
692
Gregory Haynes4fc12542015-04-22 20:38:06 -0700693 def addComment(self, message):
694 self.comments.append(message)
695 self._updateTimeStamp()
696
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200697 def getCommentAddedEvent(self, text):
698 name = 'issue_comment'
699 data = {
700 'action': 'created',
701 'issue': {
702 'number': self.number
703 },
704 'comment': {
705 'body': text
706 },
707 'repository': {
708 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100709 },
710 'sender': {
711 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200712 }
713 }
714 return (name, data)
715
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800716 def getReviewAddedEvent(self, review):
717 name = 'pull_request_review'
718 data = {
719 'action': 'submitted',
720 'pull_request': {
721 'number': self.number,
722 'title': self.subject,
723 'updated_at': self.updated_at,
724 'base': {
725 'ref': self.branch,
726 'repo': {
727 'full_name': self.project
728 }
729 },
730 'head': {
731 'sha': self.head_sha
732 }
733 },
734 'review': {
735 'state': review
736 },
737 'repository': {
738 'full_name': self.project
739 },
740 'sender': {
741 'login': 'ghuser'
742 }
743 }
744 return (name, data)
745
Jan Hruban16ad31f2015-11-07 14:39:07 +0100746 def addLabel(self, name):
747 if name not in self.labels:
748 self.labels.append(name)
749 self._updateTimeStamp()
750 return self._getLabelEvent(name)
751
752 def removeLabel(self, name):
753 if name in self.labels:
754 self.labels.remove(name)
755 self._updateTimeStamp()
756 return self._getUnlabelEvent(name)
757
758 def _getLabelEvent(self, label):
759 name = 'pull_request'
760 data = {
761 'action': 'labeled',
762 'pull_request': {
763 'number': self.number,
764 'updated_at': self.updated_at,
765 'base': {
766 'ref': self.branch,
767 'repo': {
768 'full_name': self.project
769 }
770 },
771 'head': {
772 'sha': self.head_sha
773 }
774 },
775 'label': {
776 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100777 },
778 'sender': {
779 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100780 }
781 }
782 return (name, data)
783
784 def _getUnlabelEvent(self, label):
785 name = 'pull_request'
786 data = {
787 'action': 'unlabeled',
788 'pull_request': {
789 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100790 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100791 'updated_at': self.updated_at,
792 'base': {
793 'ref': self.branch,
794 'repo': {
795 'full_name': self.project
796 }
797 },
798 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800799 'sha': self.head_sha,
800 'repo': {
801 'full_name': self.project
802 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100803 }
804 },
805 'label': {
806 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100807 },
808 'sender': {
809 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100810 }
811 }
812 return (name, data)
813
Jesse Keatinga41566f2017-06-14 18:17:51 -0700814 def editBody(self, body):
815 self.body = body
816 self._updateTimeStamp()
817
Gregory Haynes4fc12542015-04-22 20:38:06 -0700818 def _getRepo(self):
819 repo_path = os.path.join(self.upstream_root, self.project)
820 return git.Repo(repo_path)
821
822 def _createPRRef(self):
823 repo = self._getRepo()
824 GithubChangeReference.create(
825 repo, self._getPRReference(), 'refs/tags/init')
826
Jan Hruban570d01c2016-03-10 21:51:32 +0100827 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700828 repo = self._getRepo()
829 ref = repo.references[self._getPRReference()]
830 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100831 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700832 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100833 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700834 repo.head.reference = ref
835 zuul.merger.merger.reset_repo_to_head(repo)
836 repo.git.clean('-x', '-f', '-d')
837
Jan Hruban570d01c2016-03-10 21:51:32 +0100838 if files:
839 fn = files[0]
840 self.files = files
841 else:
842 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
843 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100844 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700845 fn = os.path.join(repo.working_dir, fn)
846 f = open(fn, 'w')
847 with open(fn, 'w') as f:
848 f.write("test %s %s\n" %
849 (self.branch, self.number))
850 repo.index.add([fn])
851
852 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800853 # Create an empty set of statuses for the given sha,
854 # each sha on a PR may have a status set on it
855 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700856 repo.head.reference = 'master'
857 zuul.merger.merger.reset_repo_to_head(repo)
858 repo.git.clean('-x', '-f', '-d')
859 repo.heads['master'].checkout()
860
861 def _updateTimeStamp(self):
862 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
863
864 def getPRHeadSha(self):
865 repo = self._getRepo()
866 return repo.references[self._getPRReference()].commit.hexsha
867
Jesse Keatingae4cd272017-01-30 17:10:44 -0800868 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800869 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
870 # convert the timestamp to a str format that would be returned
871 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800872
Adam Gandelmand81dd762017-02-09 15:15:49 -0800873 if granted_on:
874 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
875 submitted_at = time.strftime(
876 gh_time_format, granted_on.timetuple())
877 else:
878 # github timestamps only down to the second, so we need to make
879 # sure reviews that tests add appear to be added over a period of
880 # time in the past and not all at once.
881 if not self.reviews:
882 # the first review happens 10 mins ago
883 offset = 600
884 else:
885 # subsequent reviews happen 1 minute closer to now
886 offset = 600 - (len(self.reviews) * 60)
887
888 granted_on = datetime.datetime.utcfromtimestamp(
889 time.time() - offset)
890 submitted_at = time.strftime(
891 gh_time_format, granted_on.timetuple())
892
Jesse Keatingae4cd272017-01-30 17:10:44 -0800893 self.reviews.append({
894 'state': state,
895 'user': {
896 'login': user,
897 'email': user + "@derp.com",
898 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800899 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800900 })
901
Gregory Haynes4fc12542015-04-22 20:38:06 -0700902 def _getPRReference(self):
903 return '%s/head' % self.number
904
905 def _getPullRequestEvent(self, action):
906 name = 'pull_request'
907 data = {
908 'action': action,
909 'number': self.number,
910 'pull_request': {
911 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100912 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700913 'updated_at': self.updated_at,
914 'base': {
915 'ref': self.branch,
916 'repo': {
917 'full_name': self.project
918 }
919 },
920 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800921 'sha': self.head_sha,
922 'repo': {
923 'full_name': self.project
924 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700925 },
926 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100927 },
928 'sender': {
929 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700930 }
931 }
932 return (name, data)
933
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800934 def getCommitStatusEvent(self, context, state='success', user='zuul'):
935 name = 'status'
936 data = {
937 'state': state,
938 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700939 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800940 'description': 'Test results for %s: %s' % (self.head_sha, state),
941 'target_url': 'http://zuul/%s' % self.head_sha,
942 'branches': [],
943 'context': context,
944 'sender': {
945 'login': user
946 }
947 }
948 return (name, data)
949
James E. Blair289f5932017-07-27 15:02:29 -0700950 def setMerged(self, commit_message):
951 self.is_merged = True
952 self.merge_message = commit_message
953
954 repo = self._getRepo()
955 repo.heads[self.branch].commit = repo.commit(self.head_sha)
956
Gregory Haynes4fc12542015-04-22 20:38:06 -0700957
958class FakeGithubConnection(githubconnection.GithubConnection):
959 log = logging.getLogger("zuul.test.FakeGithubConnection")
960
Jesse Keating80730e62017-09-14 15:35:11 -0600961 def __init__(self, driver, connection_name, connection_config, rpcclient,
Tobias Henkel054eccc2017-12-20 11:36:19 +0100962 changes_db=None, upstream_root=None, git_url_with_auth=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700963 super(FakeGithubConnection, self).__init__(driver, connection_name,
964 connection_config)
965 self.connection_name = connection_name
966 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800967 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700968 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700969 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100970 self.merge_failure = False
971 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100972 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800973 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Tobias Henkel054eccc2017-12-20 11:36:19 +0100974 self.git_url_with_auth = git_url_with_auth
Jesse Keating80730e62017-09-14 15:35:11 -0600975 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200976
977 def getGithubClient(self,
978 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600979 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200980 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700981
Jesse Keating80730e62017-09-14 15:35:11 -0600982 def setZuulWebPort(self, port):
983 self.zuul_web_port = port
984
Jesse Keatinga41566f2017-06-14 18:17:51 -0700985 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700986 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700987 self.pr_number += 1
988 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100989 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700990 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800991 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700992 return pull_request
993
Jesse Keating71a47ff2017-06-06 11:36:43 -0700994 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
995 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700996 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700997 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700998 if not new_rev:
999 new_rev = random_sha1()
1000 name = 'push'
1001 data = {
1002 'ref': ref,
1003 'before': old_rev,
1004 'after': new_rev,
1005 'repository': {
1006 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001007 },
1008 'commits': [
1009 {
1010 'added': added_files,
1011 'removed': removed_files,
1012 'modified': modified_files
1013 }
1014 ]
Wayne1a78c612015-06-11 17:14:13 -07001015 }
1016 return (name, data)
1017
Jesse Keating80730e62017-09-14 15:35:11 -06001018 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001019 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001020 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001021 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001022 secret = self.connection_config['webhook_token']
1023 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001024 headers = {'x-github-event': name, 'x-hub-signature': signature}
1025
1026 if use_zuulweb:
1027 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001028 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001029 % (self.zuul_web_port, self.connection_name),
1030 data=payload, headers=headers)
1031 return urllib.request.urlopen(req)
1032 else:
1033 job = self.rpcclient.submitJob(
1034 'github:%s:payload' % self.connection_name,
1035 {'headers': headers, 'body': data})
1036 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001037
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001038 def addProject(self, project):
1039 # use the original method here and additionally register it in the
1040 # fake github
1041 super(FakeGithubConnection, self).addProject(project)
1042 self.getGithubClient(project).addProject(project)
1043
Jesse Keating9021a012017-08-29 14:45:27 -07001044 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001045 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001046 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001047 if len(prs) > 1:
1048 raise Exception('Multiple pulls found with head sha: %s' % sha)
1049 pr = prs[0]
1050 return self.getPull(pr.project, pr.number)
1051
Jesse Keatingae4cd272017-01-30 17:10:44 -08001052 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001053 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001054 return pr.reviews
1055
Jesse Keatingae4cd272017-01-30 17:10:44 -08001056 def getRepoPermission(self, project, login):
1057 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001058 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001059 pr_owner, pr_project = pr.project.split('/')
1060 if (pr_owner == owner and proj == pr_project):
1061 if login in pr.writers:
1062 return 'write'
1063 else:
1064 return 'read'
1065
Gregory Haynes4fc12542015-04-22 20:38:06 -07001066 def getGitUrl(self, project):
Tobias Henkel054eccc2017-12-20 11:36:19 +01001067 if self.git_url_with_auth:
1068 auth_token = ''.join(
1069 random.choice(string.ascii_lowercase) for x in range(8))
1070 prefix = 'file://x-access-token:%s@' % auth_token
1071 else:
1072 prefix = ''
1073 return prefix + os.path.join(self.upstream_root, str(project))
Gregory Haynes4fc12542015-04-22 20:38:06 -07001074
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001075 def real_getGitUrl(self, project):
1076 return super(FakeGithubConnection, self).getGitUrl(project)
1077
Jan Hrubane252a732017-01-03 15:03:09 +01001078 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001079 # record that this got reported
1080 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001081 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001082 pull_request.addComment(message)
1083
Jan Hruban3b415922016-02-03 13:10:22 +01001084 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001085 # record that this got reported
1086 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001087 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001088 if self.merge_failure:
1089 raise Exception('Pull request was not merged')
1090 if self.merge_not_allowed_count > 0:
1091 self.merge_not_allowed_count -= 1
1092 raise MergeFailure('Merge was not successful due to mergeability'
1093 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001094 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001095
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001096 def setCommitStatus(self, project, sha, state, url='', description='',
1097 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001098 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001099 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001100 super(FakeGithubConnection, self).setCommitStatus(
1101 project, sha, state,
1102 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001103
Jan Hruban16ad31f2015-11-07 14:39:07 +01001104 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001105 # record that this got reported
1106 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001107 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001108 pull_request.addLabel(label)
1109
1110 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001111 # record that this got reported
1112 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001113 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001114 pull_request.removeLabel(label)
1115
Gregory Haynes4fc12542015-04-22 20:38:06 -07001116
Clark Boylanb640e052014-04-03 16:41:46 -07001117class BuildHistory(object):
1118 def __init__(self, **kw):
1119 self.__dict__.update(kw)
1120
1121 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001122 return ("<Completed build, result: %s name: %s uuid: %s "
1123 "changes: %s ref: %s>" %
1124 (self.result, self.name, self.uuid,
1125 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001126
1127
Clark Boylanb640e052014-04-03 16:41:46 -07001128class FakeStatsd(threading.Thread):
1129 def __init__(self):
1130 threading.Thread.__init__(self)
1131 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001132 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001133 self.sock.bind(('', 0))
1134 self.port = self.sock.getsockname()[1]
1135 self.wake_read, self.wake_write = os.pipe()
1136 self.stats = []
1137
1138 def run(self):
1139 while True:
1140 poll = select.poll()
1141 poll.register(self.sock, select.POLLIN)
1142 poll.register(self.wake_read, select.POLLIN)
1143 ret = poll.poll()
1144 for (fd, event) in ret:
1145 if fd == self.sock.fileno():
1146 data = self.sock.recvfrom(1024)
1147 if not data:
1148 return
1149 self.stats.append(data[0])
1150 if fd == self.wake_read:
1151 return
1152
1153 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001154 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001155
1156
James E. Blaire1767bc2016-08-02 10:00:27 -07001157class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001158 log = logging.getLogger("zuul.test")
1159
Paul Belanger174a8272017-03-14 13:20:10 -04001160 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001162 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001163 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001164 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001165 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001166 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001167 # TODOv3(jeblair): self.node is really "the label of the node
1168 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001169 # keep using it like this, or we may end up exposing more of
1170 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001171 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001172 self.node = None
1173 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001174 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001175 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001176 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001177 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001178 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001179 self.wait_condition = threading.Condition()
1180 self.waiting = False
1181 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001182 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001183 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001184 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001185 items = self.parameters['zuul']['items']
1186 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1187 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001188
James E. Blair3158e282016-08-19 09:34:11 -07001189 def __repr__(self):
1190 waiting = ''
1191 if self.waiting:
1192 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001193 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1194 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001195
Clark Boylanb640e052014-04-03 16:41:46 -07001196 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001197 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001198 self.wait_condition.acquire()
1199 self.wait_condition.notify()
1200 self.waiting = False
1201 self.log.debug("Build %s released" % self.unique)
1202 self.wait_condition.release()
1203
1204 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001205 """Return whether this build is being held.
1206
1207 :returns: Whether the build is being held.
1208 :rtype: bool
1209 """
1210
Clark Boylanb640e052014-04-03 16:41:46 -07001211 self.wait_condition.acquire()
1212 if self.waiting:
1213 ret = True
1214 else:
1215 ret = False
1216 self.wait_condition.release()
1217 return ret
1218
1219 def _wait(self):
1220 self.wait_condition.acquire()
1221 self.waiting = True
1222 self.log.debug("Build %s waiting" % self.unique)
1223 self.wait_condition.wait()
1224 self.wait_condition.release()
1225
1226 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001227 self.log.debug('Running build %s' % self.unique)
1228
Paul Belanger174a8272017-03-14 13:20:10 -04001229 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001230 self.log.debug('Holding build %s' % self.unique)
1231 self._wait()
1232 self.log.debug("Build %s continuing" % self.unique)
1233
James E. Blair412fba82017-01-26 15:00:50 -08001234 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001235 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001236 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001237 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001238 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001239 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001240 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001241
James E. Blaire1767bc2016-08-02 10:00:27 -07001242 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001243
James E. Blaira5dba232016-08-08 15:53:24 -07001244 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001245 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001246 for change in changes:
1247 if self.hasChanges(change):
1248 return True
1249 return False
1250
James E. Blaire7b99a02016-08-05 14:27:34 -07001251 def hasChanges(self, *changes):
1252 """Return whether this build has certain changes in its git repos.
1253
1254 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001255 are expected to be present (in order) in the git repository of
1256 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001257
1258 :returns: Whether the build has the indicated changes.
1259 :rtype: bool
1260
1261 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001262 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001263 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001264 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001265 try:
1266 repo = git.Repo(path)
1267 except NoSuchPathError as e:
1268 self.log.debug('%s' % e)
1269 return False
James E. Blair247cab72017-07-20 16:52:36 -07001270 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001271 commit_message = '%s-1' % change.subject
1272 self.log.debug("Checking if build %s has changes; commit_message "
1273 "%s; repo_messages %s" % (self, commit_message,
1274 repo_messages))
1275 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001276 self.log.debug(" messages do not match")
1277 return False
1278 self.log.debug(" OK")
1279 return True
1280
James E. Blaird8af5422017-05-24 13:59:40 -07001281 def getWorkspaceRepos(self, projects):
1282 """Return workspace git repo objects for the listed projects
1283
1284 :arg list projects: A list of strings, each the canonical name
1285 of a project.
1286
1287 :returns: A dictionary of {name: repo} for every listed
1288 project.
1289 :rtype: dict
1290
1291 """
1292
1293 repos = {}
1294 for project in projects:
1295 path = os.path.join(self.jobdir.src_root, project)
1296 repo = git.Repo(path)
1297 repos[project] = repo
1298 return repos
1299
Clark Boylanb640e052014-04-03 16:41:46 -07001300
James E. Blair107bb252017-10-13 15:53:16 -07001301class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1302 def doMergeChanges(self, merger, items, repo_state):
1303 # Get a merger in order to update the repos involved in this job.
1304 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1305 merger, items, repo_state)
1306 if not commit: # merge conflict
1307 self.recordResult('MERGER_FAILURE')
1308 return commit
1309
1310 def recordResult(self, result):
1311 build = self.executor_server.job_builds[self.job.unique]
1312 self.executor_server.lock.acquire()
1313 self.executor_server.build_history.append(
1314 BuildHistory(name=build.name, result=result, changes=build.changes,
1315 node=build.node, uuid=build.unique,
1316 ref=build.parameters['zuul']['ref'],
1317 parameters=build.parameters, jobdir=build.jobdir,
1318 pipeline=build.parameters['zuul']['pipeline'])
1319 )
1320 self.executor_server.running_builds.remove(build)
1321 del self.executor_server.job_builds[self.job.unique]
1322 self.executor_server.lock.release()
1323
1324 def runPlaybooks(self, args):
1325 build = self.executor_server.job_builds[self.job.unique]
1326 build.jobdir = self.jobdir
1327
1328 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1329 self.recordResult(result)
1330 return result
1331
James E. Blaira86aaf12017-10-15 20:59:50 -07001332 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001333 build = self.executor_server.job_builds[self.job.unique]
1334
1335 if self.executor_server._run_ansible:
1336 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001337 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001338 else:
1339 if playbook.path:
1340 result = build.run()
1341 else:
1342 result = (self.RESULT_NORMAL, 0)
1343 return result
1344
1345 def getHostList(self, args):
1346 self.log.debug("hostlist")
1347 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1348 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001349 if not host['host_vars'].get('ansible_connection'):
1350 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001351
1352 hosts.append(dict(
James E. Blair67cd8592018-02-14 09:30:07 -08001353 name='localhost',
James E. Blair107bb252017-10-13 15:53:16 -07001354 host_vars=dict(ansible_connection='local'),
1355 host_keys=[]))
1356 return hosts
1357
1358
Paul Belanger174a8272017-03-14 13:20:10 -04001359class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1360 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001361
Paul Belanger174a8272017-03-14 13:20:10 -04001362 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001363 they will report that they have started but then pause until
1364 released before reporting completion. This attribute may be
1365 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001366 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001367 be explicitly released.
1368
1369 """
James E. Blairfaf81982017-10-10 15:42:26 -07001370
1371 _job_class = RecordingAnsibleJob
1372
James E. Blairf5dbd002015-12-23 15:26:17 -08001373 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001374 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001375 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001376 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001377 self.hold_jobs_in_build = False
1378 self.lock = threading.Lock()
1379 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001380 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001381 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001382 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001383
James E. Blaira5dba232016-08-08 15:53:24 -07001384 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001385 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001386
1387 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001388 :arg Change change: The :py:class:`~tests.base.FakeChange`
1389 instance which should cause the job to fail. This job
1390 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001391
1392 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001393 l = self.fail_tests.get(name, [])
1394 l.append(change)
1395 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001396
James E. Blair962220f2016-08-03 11:22:38 -07001397 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001398 """Release a held build.
1399
1400 :arg str regex: A regular expression which, if supplied, will
1401 cause only builds with matching names to be released. If
1402 not supplied, all builds will be released.
1403
1404 """
James E. Blair962220f2016-08-03 11:22:38 -07001405 builds = self.running_builds[:]
1406 self.log.debug("Releasing build %s (%s)" % (regex,
1407 len(self.running_builds)))
1408 for build in builds:
1409 if not regex or re.match(regex, build.name):
1410 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001411 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001412 build.release()
1413 else:
1414 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001415 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001416 self.log.debug("Done releasing builds %s (%s)" %
1417 (regex, len(self.running_builds)))
1418
Paul Belanger174a8272017-03-14 13:20:10 -04001419 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001420 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001421 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001422 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001423 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001424 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001425 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001426 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001427 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001428
1429 def stopJob(self, job):
1430 self.log.debug("handle stop")
1431 parameters = json.loads(job.arguments)
1432 uuid = parameters['uuid']
1433 for build in self.running_builds:
1434 if build.unique == uuid:
1435 build.aborted = True
1436 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001437 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001438
James E. Blaira002b032017-04-18 10:35:48 -07001439 def stop(self):
1440 for build in self.running_builds:
1441 build.release()
1442 super(RecordingExecutorServer, self).stop()
1443
Joshua Hesketh50c21782016-10-13 21:34:14 +11001444
Clark Boylanb640e052014-04-03 16:41:46 -07001445class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001446 """A Gearman server for use in tests.
1447
1448 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1449 added to the queue but will not be distributed to workers
1450 until released. This attribute may be changed at any time and
1451 will take effect for subsequently enqueued jobs, but
1452 previously held jobs will still need to be explicitly
1453 released.
1454
1455 """
1456
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001457 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001458 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001459 self.hold_merge_jobs_in_queue = False
Fabien Boucher52252312018-01-18 19:54:34 +01001460 self.jobs_history = []
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001461 if use_ssl:
1462 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1463 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1464 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1465 else:
1466 ssl_ca = None
1467 ssl_cert = None
1468 ssl_key = None
1469
1470 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1471 ssl_cert=ssl_cert,
1472 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001473
1474 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001475 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1476 for job in job_queue:
Fabien Boucher52252312018-01-18 19:54:34 +01001477 self.jobs_history.append(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001478 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001479 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001480 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001481 elif job.name.startswith(b'merger:'):
1482 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001483 else:
1484 job.waiting = False
1485 if job.waiting:
1486 continue
1487 if job.name in connection.functions:
1488 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001489 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001490 connection.related_jobs[job.handle] = job
1491 job.worker_connection = connection
1492 job.running = True
1493 return job
1494 return None
1495
1496 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001497 """Release a held job.
1498
1499 :arg str regex: A regular expression which, if supplied, will
1500 cause only jobs with matching names to be released. If
1501 not supplied, all jobs will be released.
1502 """
Clark Boylanb640e052014-04-03 16:41:46 -07001503 released = False
1504 qlen = (len(self.high_queue) + len(self.normal_queue) +
1505 len(self.low_queue))
1506 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1507 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001508 match = False
1509 if job.name == b'executor:execute':
1510 parameters = json.loads(job.arguments.decode('utf8'))
1511 if not regex or re.match(regex, parameters.get('job')):
1512 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001513 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001514 if not regex:
1515 match = True
1516 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001517 self.log.debug("releasing queued job %s" %
1518 job.unique)
1519 job.waiting = False
1520 released = True
1521 else:
1522 self.log.debug("not releasing queued job %s" %
1523 job.unique)
1524 if released:
1525 self.wakeConnections()
1526 qlen = (len(self.high_queue) + len(self.normal_queue) +
1527 len(self.low_queue))
1528 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1529
1530
1531class FakeSMTP(object):
1532 log = logging.getLogger('zuul.FakeSMTP')
1533
1534 def __init__(self, messages, server, port):
1535 self.server = server
1536 self.port = port
1537 self.messages = messages
1538
1539 def sendmail(self, from_email, to_email, msg):
1540 self.log.info("Sending email from %s, to %s, with msg %s" % (
1541 from_email, to_email, msg))
1542
1543 headers = msg.split('\n\n', 1)[0]
1544 body = msg.split('\n\n', 1)[1]
1545
1546 self.messages.append(dict(
1547 from_email=from_email,
1548 to_email=to_email,
1549 msg=msg,
1550 headers=headers,
1551 body=body,
1552 ))
1553
1554 return True
1555
1556 def quit(self):
1557 return True
1558
1559
James E. Blairdce6cea2016-12-20 16:45:32 -08001560class FakeNodepool(object):
1561 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001562 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001563
1564 log = logging.getLogger("zuul.test.FakeNodepool")
1565
1566 def __init__(self, host, port, chroot):
1567 self.client = kazoo.client.KazooClient(
1568 hosts='%s:%s%s' % (host, port, chroot))
1569 self.client.start()
1570 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001571 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001572 self.thread = threading.Thread(target=self.run)
1573 self.thread.daemon = True
1574 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001575 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001576
1577 def stop(self):
1578 self._running = False
1579 self.thread.join()
1580 self.client.stop()
1581 self.client.close()
1582
1583 def run(self):
1584 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001585 try:
1586 self._run()
1587 except Exception:
1588 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001589 time.sleep(0.1)
1590
1591 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001592 if self.paused:
1593 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001594 for req in self.getNodeRequests():
1595 self.fulfillRequest(req)
1596
1597 def getNodeRequests(self):
1598 try:
1599 reqids = self.client.get_children(self.REQUEST_ROOT)
1600 except kazoo.exceptions.NoNodeError:
1601 return []
1602 reqs = []
1603 for oid in sorted(reqids):
1604 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001605 try:
1606 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001607 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001608 data['_oid'] = oid
1609 reqs.append(data)
1610 except kazoo.exceptions.NoNodeError:
1611 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001612 return reqs
1613
James E. Blaire18d4602017-01-05 11:17:28 -08001614 def getNodes(self):
1615 try:
1616 nodeids = self.client.get_children(self.NODE_ROOT)
1617 except kazoo.exceptions.NoNodeError:
1618 return []
1619 nodes = []
1620 for oid in sorted(nodeids):
1621 path = self.NODE_ROOT + '/' + oid
1622 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001623 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001624 data['_oid'] = oid
1625 try:
1626 lockfiles = self.client.get_children(path + '/lock')
1627 except kazoo.exceptions.NoNodeError:
1628 lockfiles = []
1629 if lockfiles:
1630 data['_lock'] = True
1631 else:
1632 data['_lock'] = False
1633 nodes.append(data)
1634 return nodes
1635
James E. Blaira38c28e2017-01-04 10:33:20 -08001636 def makeNode(self, request_id, node_type):
1637 now = time.time()
1638 path = '/nodepool/nodes/'
1639 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001640 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001641 provider='test-provider',
1642 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001643 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001644 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001645 public_ipv4='127.0.0.1',
1646 private_ipv4=None,
1647 public_ipv6=None,
1648 allocated_to=request_id,
1649 state='ready',
1650 state_time=now,
1651 created_time=now,
1652 updated_time=now,
1653 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001654 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001655 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001656 if 'fakeuser' in node_type:
1657 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001658 if 'windows' in node_type:
1659 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001660 if 'network' in node_type:
1661 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001662
Clint Byrumf322fe22017-05-10 20:53:12 -07001663 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001664 path = self.client.create(path, data,
1665 makepath=True,
1666 sequence=True)
1667 nodeid = path.split("/")[-1]
1668 return nodeid
1669
Krzysztof Klimonda37d54032017-10-25 12:16:47 +02001670 def removeNode(self, node):
1671 path = self.NODE_ROOT + '/' + node["_oid"]
1672 self.client.delete(path, recursive=True)
1673
James E. Blair6ab79e02017-01-06 10:10:17 -08001674 def addFailRequest(self, request):
1675 self.fail_requests.add(request['_oid'])
1676
James E. Blairdce6cea2016-12-20 16:45:32 -08001677 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001678 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001679 return
1680 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001681 oid = request['_oid']
1682 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001683
James E. Blair6ab79e02017-01-06 10:10:17 -08001684 if oid in self.fail_requests:
1685 request['state'] = 'failed'
1686 else:
1687 request['state'] = 'fulfilled'
1688 nodes = []
1689 for node in request['node_types']:
1690 nodeid = self.makeNode(oid, node)
1691 nodes.append(nodeid)
1692 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001693
James E. Blaira38c28e2017-01-04 10:33:20 -08001694 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001695 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001696 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001697 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001698 try:
1699 self.client.set(path, data)
1700 except kazoo.exceptions.NoNodeError:
1701 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001702
1703
James E. Blair498059b2016-12-20 13:50:13 -08001704class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001705 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001706 super(ChrootedKazooFixture, self).__init__()
1707
1708 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1709 if ':' in zk_host:
1710 host, port = zk_host.split(':')
1711 else:
1712 host = zk_host
1713 port = None
1714
1715 self.zookeeper_host = host
1716
1717 if not port:
1718 self.zookeeper_port = 2181
1719 else:
1720 self.zookeeper_port = int(port)
1721
Clark Boylan621ec9a2017-04-07 17:41:33 -07001722 self.test_id = test_id
1723
James E. Blair498059b2016-12-20 13:50:13 -08001724 def _setUp(self):
1725 # Make sure the test chroot paths do not conflict
1726 random_bits = ''.join(random.choice(string.ascii_lowercase +
1727 string.ascii_uppercase)
1728 for x in range(8))
1729
Clark Boylan621ec9a2017-04-07 17:41:33 -07001730 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001731 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1732
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001733 self.addCleanup(self._cleanup)
1734
James E. Blair498059b2016-12-20 13:50:13 -08001735 # Ensure the chroot path exists and clean up any pre-existing znodes.
1736 _tmp_client = kazoo.client.KazooClient(
1737 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1738 _tmp_client.start()
1739
1740 if _tmp_client.exists(self.zookeeper_chroot):
1741 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1742
1743 _tmp_client.ensure_path(self.zookeeper_chroot)
1744 _tmp_client.stop()
1745 _tmp_client.close()
1746
James E. Blair498059b2016-12-20 13:50:13 -08001747 def _cleanup(self):
1748 '''Remove the chroot path.'''
1749 # Need a non-chroot'ed client to remove the chroot path
1750 _tmp_client = kazoo.client.KazooClient(
1751 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1752 _tmp_client.start()
1753 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1754 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001755 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001756
1757
Joshua Heskethd78b4482015-09-14 16:56:34 -06001758class MySQLSchemaFixture(fixtures.Fixture):
1759 def setUp(self):
1760 super(MySQLSchemaFixture, self).setUp()
1761
1762 random_bits = ''.join(random.choice(string.ascii_lowercase +
1763 string.ascii_uppercase)
1764 for x in range(8))
1765 self.name = '%s_%s' % (random_bits, os.getpid())
1766 self.passwd = uuid.uuid4().hex
1767 db = pymysql.connect(host="localhost",
1768 user="openstack_citest",
1769 passwd="openstack_citest",
1770 db="openstack_citest")
1771 cur = db.cursor()
1772 cur.execute("create database %s" % self.name)
1773 cur.execute(
1774 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1775 (self.name, self.name, self.passwd))
1776 cur.execute("flush privileges")
1777
1778 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1779 self.passwd,
1780 self.name)
1781 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1782 self.addCleanup(self.cleanup)
1783
1784 def cleanup(self):
1785 db = pymysql.connect(host="localhost",
1786 user="openstack_citest",
1787 passwd="openstack_citest",
1788 db="openstack_citest")
1789 cur = db.cursor()
1790 cur.execute("drop database %s" % self.name)
1791 cur.execute("drop user '%s'@'localhost'" % self.name)
1792 cur.execute("flush privileges")
1793
1794
Maru Newby3fe5f852015-01-13 04:22:14 +00001795class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001796 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001797 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001798
James E. Blair1c236df2017-02-01 14:07:24 -08001799 def attachLogs(self, *args):
1800 def reader():
1801 self._log_stream.seek(0)
1802 while True:
1803 x = self._log_stream.read(4096)
1804 if not x:
1805 break
1806 yield x.encode('utf8')
1807 content = testtools.content.content_from_reader(
1808 reader,
1809 testtools.content_type.UTF8_TEXT,
1810 False)
1811 self.addDetail('logging', content)
1812
Clark Boylanb640e052014-04-03 16:41:46 -07001813 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001814 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001815 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1816 try:
1817 test_timeout = int(test_timeout)
1818 except ValueError:
1819 # If timeout value is invalid do not set a timeout.
1820 test_timeout = 0
1821 if test_timeout > 0:
1822 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1823
1824 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1825 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1826 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1827 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1828 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1829 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1830 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1831 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1832 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1833 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001834 self._log_stream = StringIO()
1835 self.addOnException(self.attachLogs)
1836 else:
1837 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001838
James E. Blair1c236df2017-02-01 14:07:24 -08001839 handler = logging.StreamHandler(self._log_stream)
1840 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1841 '%(levelname)-8s %(message)s')
1842 handler.setFormatter(formatter)
1843
1844 logger = logging.getLogger()
1845 logger.setLevel(logging.DEBUG)
1846 logger.addHandler(handler)
1847
Clark Boylan3410d532017-04-25 12:35:29 -07001848 # Make sure we don't carry old handlers around in process state
1849 # which slows down test runs
1850 self.addCleanup(logger.removeHandler, handler)
1851 self.addCleanup(handler.close)
1852 self.addCleanup(handler.flush)
1853
James E. Blair1c236df2017-02-01 14:07:24 -08001854 # NOTE(notmorgan): Extract logging overrides for specific
1855 # libraries from the OS_LOG_DEFAULTS env and create loggers
1856 # for each. This is used to limit the output during test runs
1857 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001858 log_defaults_from_env = os.environ.get(
1859 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001860 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001861
James E. Blairdce6cea2016-12-20 16:45:32 -08001862 if log_defaults_from_env:
1863 for default in log_defaults_from_env.split(','):
1864 try:
1865 name, level_str = default.split('=', 1)
1866 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001867 logger = logging.getLogger(name)
1868 logger.setLevel(level)
1869 logger.addHandler(handler)
1870 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001871 except ValueError:
1872 # NOTE(notmorgan): Invalid format of the log default,
1873 # skip and don't try and apply a logger for the
1874 # specified module
1875 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001876
Maru Newby3fe5f852015-01-13 04:22:14 +00001877
1878class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001879 """A test case with a functioning Zuul.
1880
1881 The following class variables are used during test setup and can
1882 be overidden by subclasses but are effectively read-only once a
1883 test method starts running:
1884
1885 :cvar str config_file: This points to the main zuul config file
1886 within the fixtures directory. Subclasses may override this
1887 to obtain a different behavior.
1888
1889 :cvar str tenant_config_file: This is the tenant config file
1890 (which specifies from what git repos the configuration should
1891 be loaded). It defaults to the value specified in
1892 `config_file` but can be overidden by subclasses to obtain a
1893 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001894 configuration. See also the :py:func:`simple_layout`
1895 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001896
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001897 :cvar bool create_project_keys: Indicates whether Zuul should
1898 auto-generate keys for each project, or whether the test
1899 infrastructure should insert dummy keys to save time during
1900 startup. Defaults to False.
1901
James E. Blaire7b99a02016-08-05 14:27:34 -07001902 The following are instance variables that are useful within test
1903 methods:
1904
1905 :ivar FakeGerritConnection fake_<connection>:
1906 A :py:class:`~tests.base.FakeGerritConnection` will be
1907 instantiated for each connection present in the config file
1908 and stored here. For instance, `fake_gerrit` will hold the
1909 FakeGerritConnection object for a connection named `gerrit`.
1910
1911 :ivar FakeGearmanServer gearman_server: An instance of
1912 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1913 server that all of the Zuul components in this test use to
1914 communicate with each other.
1915
Paul Belanger174a8272017-03-14 13:20:10 -04001916 :ivar RecordingExecutorServer executor_server: An instance of
1917 :py:class:`~tests.base.RecordingExecutorServer` which is the
1918 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001919
1920 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1921 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001922 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001923 list upon completion.
1924
1925 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1926 objects representing completed builds. They are appended to
1927 the list in the order they complete.
1928
1929 """
1930
James E. Blair83005782015-12-11 14:46:03 -08001931 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001932 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001933 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001934 use_ssl = False
Tobias Henkel054eccc2017-12-20 11:36:19 +01001935 git_url_with_auth = False
James E. Blair3f876d52016-07-22 13:07:14 -07001936
1937 def _startMerger(self):
1938 self.merge_server = zuul.merger.server.MergeServer(self.config,
1939 self.connections)
1940 self.merge_server.start()
1941
Maru Newby3fe5f852015-01-13 04:22:14 +00001942 def setUp(self):
1943 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001944
1945 self.setupZK()
1946
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001947 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001948 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001949 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1950 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001951 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001952 tmp_root = tempfile.mkdtemp(
1953 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001954 self.test_root = os.path.join(tmp_root, "zuul-test")
1955 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001956 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001957 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001958 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001959 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1960 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001961
1962 if os.path.exists(self.test_root):
1963 shutil.rmtree(self.test_root)
1964 os.makedirs(self.test_root)
1965 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001966 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001967 os.makedirs(self.merger_state_root)
1968 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001969
1970 # Make per test copy of Configuration.
1971 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001972 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1973 if not os.path.exists(self.private_key_file):
1974 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1975 shutil.copy(src_private_key_file, self.private_key_file)
1976 shutil.copy('{}.pub'.format(src_private_key_file),
1977 '{}.pub'.format(self.private_key_file))
1978 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001979 self.config.set('scheduler', 'tenant_config',
1980 os.path.join(
1981 FIXTURE_DIR,
1982 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001983 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001984 self.config.set(
1985 'scheduler', 'command_socket',
1986 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001987 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001988 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001989 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001990 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001991 self.config.set(
1992 'executor', 'command_socket',
1993 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001994 self.config.set(
1995 'merger', 'command_socket',
1996 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001997
Clark Boylanb640e052014-04-03 16:41:46 -07001998 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001999 if self.config.has_section('statsd'):
2000 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002001 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002002
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002003 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002004
2005 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002006 self.log.info("Gearman server on port %s" %
2007 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002008 if self.use_ssl:
2009 self.log.info('SSL enabled for gearman')
2010 self.config.set(
2011 'gearman', 'ssl_ca',
2012 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2013 self.config.set(
2014 'gearman', 'ssl_cert',
2015 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2016 self.config.set(
2017 'gearman', 'ssl_key',
2018 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002019
Jesse Keating80730e62017-09-14 15:35:11 -06002020 self.rpcclient = zuul.rpcclient.RPCClient(
2021 self.config.get('gearman', 'server'),
2022 self.gearman_server.port,
2023 get_default(self.config, 'gearman', 'ssl_key'),
2024 get_default(self.config, 'gearman', 'ssl_cert'),
2025 get_default(self.config, 'gearman', 'ssl_ca'))
2026
James E. Blaire511d2f2016-12-08 15:22:26 -08002027 gerritsource.GerritSource.replication_timeout = 1.5
2028 gerritsource.GerritSource.replication_retry_interval = 0.5
2029 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002030
Joshua Hesketh352264b2015-08-11 23:42:08 +10002031 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002032 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002033
Jan Hruban6b71aff2015-10-22 16:58:08 +02002034 self.event_queues = [
2035 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002036 self.sched.trigger_event_queue,
2037 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002038 ]
2039
James E. Blairfef78942016-03-11 16:28:56 -08002040 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002041 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002042
Paul Belanger174a8272017-03-14 13:20:10 -04002043 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002044 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002045 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002046 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002047 _test_root=self.test_root,
2048 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002049 self.executor_server.start()
2050 self.history = self.executor_server.build_history
2051 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002052
Paul Belanger174a8272017-03-14 13:20:10 -04002053 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002054 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002055 self.merge_client = zuul.merger.client.MergeClient(
2056 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002057 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002058 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002059 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002060 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002061
James E. Blair0d5a36e2017-02-21 10:53:44 -05002062 self.fake_nodepool = FakeNodepool(
2063 self.zk_chroot_fixture.zookeeper_host,
2064 self.zk_chroot_fixture.zookeeper_port,
2065 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002066
Paul Belanger174a8272017-03-14 13:20:10 -04002067 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002068 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002069 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002070 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002071
Clark Boylanb640e052014-04-03 16:41:46 -07002072 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002073 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002074 # Cleanups are run in reverse order
2075 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002076 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002077 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002078
James E. Blairb9c0d772017-03-03 14:34:49 -08002079 self.sched.reconfigure(self.config)
2080 self.sched.resume()
2081
Tobias Henkel7df274b2017-05-26 17:41:11 +02002082 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002083 # Set up gerrit related fakes
2084 # Set a changes database so multiple FakeGerrit's can report back to
2085 # a virtual canonical database given by the configured hostname
2086 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002087 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002088
2089 def getGerritConnection(driver, name, config):
2090 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2091 con = FakeGerritConnection(driver, name, config,
2092 changes_db=db,
2093 upstream_root=self.upstream_root)
2094 self.event_queues.append(con.event_queue)
2095 setattr(self, 'fake_' + name, con)
2096 return con
2097
2098 self.useFixture(fixtures.MonkeyPatch(
2099 'zuul.driver.gerrit.GerritDriver.getConnection',
2100 getGerritConnection))
2101
Gregory Haynes4fc12542015-04-22 20:38:06 -07002102 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002103 server = config.get('server', 'github.com')
2104 db = self.github_changes_dbs.setdefault(server, {})
Tobias Henkel054eccc2017-12-20 11:36:19 +01002105 con = FakeGithubConnection(
2106 driver, name, config,
2107 self.rpcclient,
2108 changes_db=db,
2109 upstream_root=self.upstream_root,
2110 git_url_with_auth=self.git_url_with_auth)
Jesse Keating64d29012017-09-06 12:27:49 -07002111 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002112 setattr(self, 'fake_' + name, con)
2113 return con
2114
2115 self.useFixture(fixtures.MonkeyPatch(
2116 'zuul.driver.github.GithubDriver.getConnection',
2117 getGithubConnection))
2118
James E. Blaire511d2f2016-12-08 15:22:26 -08002119 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002120 # TODO(jhesketh): This should come from lib.connections for better
2121 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002122 # Register connections from the config
2123 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002124
Joshua Hesketh352264b2015-08-11 23:42:08 +10002125 def FakeSMTPFactory(*args, **kw):
2126 args = [self.smtp_messages] + list(args)
2127 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002128
Joshua Hesketh352264b2015-08-11 23:42:08 +10002129 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002130
James E. Blaire511d2f2016-12-08 15:22:26 -08002131 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002132 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002133 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002134
James E. Blair83005782015-12-11 14:46:03 -08002135 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002136 # This creates the per-test configuration object. It can be
2137 # overriden by subclasses, but should not need to be since it
2138 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002139 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002140 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002141
James E. Blair39840362017-06-23 20:34:02 +01002142 sections = ['zuul', 'scheduler', 'executor', 'merger']
2143 for section in sections:
2144 if not self.config.has_section(section):
2145 self.config.add_section(section)
2146
James E. Blair06cc3922017-04-19 10:08:10 -07002147 if not self.setupSimpleLayout():
2148 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002149 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002150 self.tenant_config_file)
2151 git_path = os.path.join(
2152 os.path.dirname(
2153 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2154 'git')
2155 if os.path.exists(git_path):
2156 for reponame in os.listdir(git_path):
2157 project = reponame.replace('_', '/')
2158 self.copyDirToRepo(project,
2159 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002160 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002161 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002162 self.setupAllProjectKeys()
2163
James E. Blair06cc3922017-04-19 10:08:10 -07002164 def setupSimpleLayout(self):
2165 # If the test method has been decorated with a simple_layout,
2166 # use that instead of the class tenant_config_file. Set up a
2167 # single config-project with the specified layout, and
2168 # initialize repos for all of the 'project' entries which
2169 # appear in the layout.
2170 test_name = self.id().split('.')[-1]
2171 test = getattr(self, test_name)
2172 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002173 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002174 else:
2175 return False
2176
James E. Blairb70e55a2017-04-19 12:57:02 -07002177 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002178 path = os.path.join(FIXTURE_DIR, path)
2179 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002180 data = f.read()
2181 layout = yaml.safe_load(data)
2182 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002183 untrusted_projects = []
2184 for item in layout:
2185 if 'project' in item:
2186 name = item['project']['name']
2187 untrusted_projects.append(name)
2188 self.init_repo(name)
2189 self.addCommitToRepo(name, 'initial commit',
2190 files={'README': ''},
2191 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002192 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002193 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002194 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002195 for fn in zuul.configloader.as_list(
2196 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002197 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002198 for fn in zuul.configloader.as_list(
2199 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002200 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002201
2202 root = os.path.join(self.test_root, "config")
2203 if not os.path.exists(root):
2204 os.makedirs(root)
2205 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2206 config = [{'tenant':
2207 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002208 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002209 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002210 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002211 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002212 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002213 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002214 os.path.join(FIXTURE_DIR, f.name))
2215
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002216 self.init_repo('org/common-config')
2217 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002218 files, branch='master', tag='init')
2219
2220 return True
2221
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002222 def setupAllProjectKeys(self):
2223 if self.create_project_keys:
2224 return
2225
James E. Blair39840362017-06-23 20:34:02 +01002226 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002227 with open(os.path.join(FIXTURE_DIR, path)) as f:
2228 tenant_config = yaml.safe_load(f.read())
2229 for tenant in tenant_config:
2230 sources = tenant['tenant']['source']
2231 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002232 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002233 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002234 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002235 self.setupProjectKeys(source, project)
2236
2237 def setupProjectKeys(self, source, project):
2238 # Make sure we set up an RSA key for the project so that we
2239 # don't spend time generating one:
2240
James E. Blair6459db12017-06-29 14:57:20 -07002241 if isinstance(project, dict):
2242 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002243 key_root = os.path.join(self.state_root, 'keys')
2244 if not os.path.isdir(key_root):
2245 os.mkdir(key_root, 0o700)
2246 private_key_file = os.path.join(key_root, source, project + '.pem')
2247 private_key_dir = os.path.dirname(private_key_file)
2248 self.log.debug("Installing test keys for project %s at %s" % (
2249 project, private_key_file))
2250 if not os.path.isdir(private_key_dir):
2251 os.makedirs(private_key_dir)
2252 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2253 with open(private_key_file, 'w') as o:
2254 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002255
James E. Blair498059b2016-12-20 13:50:13 -08002256 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002257 self.zk_chroot_fixture = self.useFixture(
2258 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002259 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002260 self.zk_chroot_fixture.zookeeper_host,
2261 self.zk_chroot_fixture.zookeeper_port,
2262 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002263
James E. Blair96c6bf82016-01-15 16:20:40 -08002264 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002265 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002266
2267 files = {}
2268 for (dirpath, dirnames, filenames) in os.walk(source_path):
2269 for filename in filenames:
2270 test_tree_filepath = os.path.join(dirpath, filename)
2271 common_path = os.path.commonprefix([test_tree_filepath,
2272 source_path])
2273 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2274 with open(test_tree_filepath, 'r') as f:
2275 content = f.read()
2276 files[relative_filepath] = content
2277 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002278 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002279
James E. Blaire18d4602017-01-05 11:17:28 -08002280 def assertNodepoolState(self):
2281 # Make sure that there are no pending requests
2282
2283 requests = self.fake_nodepool.getNodeRequests()
2284 self.assertEqual(len(requests), 0)
2285
2286 nodes = self.fake_nodepool.getNodes()
2287 for node in nodes:
2288 self.assertFalse(node['_lock'], "Node %s is locked" %
2289 (node['_oid'],))
2290
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002291 def assertNoGeneratedKeys(self):
2292 # Make sure that Zuul did not generate any project keys
2293 # (unless it was supposed to).
2294
2295 if self.create_project_keys:
2296 return
2297
2298 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2299 test_key = i.read()
2300
2301 key_root = os.path.join(self.state_root, 'keys')
2302 for root, dirname, files in os.walk(key_root):
2303 for fn in files:
2304 with open(os.path.join(root, fn)) as f:
2305 self.assertEqual(test_key, f.read())
2306
Clark Boylanb640e052014-04-03 16:41:46 -07002307 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002308 self.log.debug("Assert final state")
2309 # Make sure no jobs are running
2310 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002311 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002312 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002313 gc.collect()
2314 for obj in gc.get_objects():
2315 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002316 self.log.debug("Leaked git repo object: 0x%x %s" %
2317 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002318 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002319 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002320 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002321 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002322 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002323 for tenant in self.sched.abide.tenants.values():
2324 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002325 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002326 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002327
2328 def shutdown(self):
2329 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002330 self.executor_server.hold_jobs_in_build = False
2331 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002332 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002333 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002334 if self.merge_server:
2335 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002336 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002337 self.sched.stop()
2338 self.sched.join()
2339 self.statsd.stop()
2340 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002341 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002342 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002343 self.fake_nodepool.stop()
2344 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002345 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002346 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002347 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002348 # Further the pydevd threads also need to be whitelisted so debugging
2349 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002350 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002351 'pydevd.CommandThread',
2352 'pydevd.Reader',
2353 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002354 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002355 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002356 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002357 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002358 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002359 log_str = ""
2360 for thread_id, stack_frame in sys._current_frames().items():
2361 log_str += "Thread: %s\n" % thread_id
2362 log_str += "".join(traceback.format_stack(stack_frame))
2363 self.log.debug(log_str)
2364 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002365
James E. Blaira002b032017-04-18 10:35:48 -07002366 def assertCleanShutdown(self):
2367 pass
2368
James E. Blairc4ba97a2017-04-19 16:26:24 -07002369 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002370 parts = project.split('/')
2371 path = os.path.join(self.upstream_root, *parts[:-1])
2372 if not os.path.exists(path):
2373 os.makedirs(path)
2374 path = os.path.join(self.upstream_root, project)
2375 repo = git.Repo.init(path)
2376
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002377 with repo.config_writer() as config_writer:
2378 config_writer.set_value('user', 'email', 'user@example.com')
2379 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002380
Clark Boylanb640e052014-04-03 16:41:46 -07002381 repo.index.commit('initial commit')
2382 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002383 if tag:
2384 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002385
James E. Blair97d902e2014-08-21 13:25:56 -07002386 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002387 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002388 repo.git.clean('-x', '-f', '-d')
2389
James E. Blair97d902e2014-08-21 13:25:56 -07002390 def create_branch(self, project, branch):
2391 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002392 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002393 fn = os.path.join(path, 'README')
2394
2395 branch_head = repo.create_head(branch)
2396 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002397 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002398 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002399 f.close()
2400 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002401 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002402
James E. Blair97d902e2014-08-21 13:25:56 -07002403 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002404 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002405 repo.git.clean('-x', '-f', '-d')
2406
James E. Blairda5bb7e2018-01-22 16:12:17 -08002407 def delete_branch(self, project, branch):
2408 path = os.path.join(self.upstream_root, project)
2409 repo = git.Repo(path)
2410 repo.head.reference = repo.heads['master']
2411 zuul.merger.merger.reset_repo_to_head(repo)
2412 repo.delete_head(repo.heads[branch], force=True)
2413
Sachi King9f16d522016-03-16 12:20:45 +11002414 def create_commit(self, project):
2415 path = os.path.join(self.upstream_root, project)
2416 repo = git.Repo(path)
2417 repo.head.reference = repo.heads['master']
2418 file_name = os.path.join(path, 'README')
2419 with open(file_name, 'a') as f:
2420 f.write('creating fake commit\n')
2421 repo.index.add([file_name])
2422 commit = repo.index.commit('Creating a fake commit')
2423 return commit.hexsha
2424
James E. Blairf4a5f022017-04-18 14:01:10 -07002425 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002426 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002427 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002428 while len(self.builds):
2429 self.release(self.builds[0])
2430 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002431 i += 1
2432 if count is not None and i >= count:
2433 break
James E. Blairb8c16472015-05-05 14:55:26 -07002434
James E. Blairdf25ddc2017-07-08 07:57:09 -07002435 def getSortedBuilds(self):
2436 "Return the list of currently running builds sorted by name"
2437
2438 return sorted(self.builds, key=lambda x: x.name)
2439
Clark Boylanb640e052014-04-03 16:41:46 -07002440 def release(self, job):
2441 if isinstance(job, FakeBuild):
2442 job.release()
2443 else:
2444 job.waiting = False
2445 self.log.debug("Queued job %s released" % job.unique)
2446 self.gearman_server.wakeConnections()
2447
2448 def getParameter(self, job, name):
2449 if isinstance(job, FakeBuild):
2450 return job.parameters[name]
2451 else:
2452 parameters = json.loads(job.arguments)
2453 return parameters[name]
2454
Clark Boylanb640e052014-04-03 16:41:46 -07002455 def haveAllBuildsReported(self):
2456 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002457 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002458 return False
2459 # Find out if every build that the worker has completed has been
2460 # reported back to Zuul. If it hasn't then that means a Gearman
2461 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002462 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002463 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002464 if not zbuild:
2465 # It has already been reported
2466 continue
2467 # It hasn't been reported yet.
2468 return False
2469 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002470 worker = self.executor_server.executor_worker
2471 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002472 if connection.state == 'GRAB_WAIT':
2473 return False
2474 return True
2475
2476 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002477 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002478 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002479 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002480 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002481 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002482 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002483 for j in conn.related_jobs.values():
2484 if j.unique == build.uuid:
2485 client_job = j
2486 break
2487 if not client_job:
2488 self.log.debug("%s is not known to the gearman client" %
2489 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002490 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002491 if not client_job.handle:
2492 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002493 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002494 server_job = self.gearman_server.jobs.get(client_job.handle)
2495 if not server_job:
2496 self.log.debug("%s is not known to the gearman server" %
2497 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002498 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002499 if not hasattr(server_job, 'waiting'):
2500 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002501 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002502 if server_job.waiting:
2503 continue
James E. Blair17302972016-08-10 16:11:42 -07002504 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002505 self.log.debug("%s has not reported start" % build)
2506 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002507 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002508 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002509 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002510 if worker_build:
2511 if worker_build.isWaiting():
2512 continue
2513 else:
2514 self.log.debug("%s is running" % worker_build)
2515 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002516 else:
James E. Blair962220f2016-08-03 11:22:38 -07002517 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002518 return False
James E. Blaira002b032017-04-18 10:35:48 -07002519 for (build_uuid, job_worker) in \
2520 self.executor_server.job_workers.items():
2521 if build_uuid not in seen_builds:
2522 self.log.debug("%s is not finalized" % build_uuid)
2523 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002524 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002525
James E. Blairdce6cea2016-12-20 16:45:32 -08002526 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002527 if self.fake_nodepool.paused:
2528 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002529 if self.sched.nodepool.requests:
2530 return False
2531 return True
2532
James E. Blaira615c362017-10-02 17:34:42 -07002533 def areAllMergeJobsWaiting(self):
2534 for client_job in list(self.merge_client.jobs):
2535 if not client_job.handle:
2536 self.log.debug("%s has no handle" % client_job)
2537 return False
2538 server_job = self.gearman_server.jobs.get(client_job.handle)
2539 if not server_job:
2540 self.log.debug("%s is not known to the gearman server" %
2541 client_job)
2542 return False
2543 if not hasattr(server_job, 'waiting'):
2544 self.log.debug("%s is being enqueued" % server_job)
2545 return False
2546 if server_job.waiting:
2547 self.log.debug("%s is waiting" % server_job)
2548 continue
2549 self.log.debug("%s is not waiting" % server_job)
2550 return False
2551 return True
2552
Jan Hruban6b71aff2015-10-22 16:58:08 +02002553 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002554 for event_queue in self.event_queues:
2555 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002556
2557 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002558 for event_queue in self.event_queues:
2559 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002560
Clark Boylanb640e052014-04-03 16:41:46 -07002561 def waitUntilSettled(self):
2562 self.log.debug("Waiting until settled...")
2563 start = time.time()
2564 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002565 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002566 self.log.error("Timeout waiting for Zuul to settle")
2567 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002568 for event_queue in self.event_queues:
2569 self.log.error(" %s: %s" %
2570 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002571 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002572 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002573 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002574 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002575 self.log.error("All requests completed: %s" %
2576 (self.areAllNodeRequestsComplete(),))
2577 self.log.error("Merge client jobs: %s" %
2578 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002579 raise Exception("Timeout waiting for Zuul to settle")
2580 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002581
Paul Belanger174a8272017-03-14 13:20:10 -04002582 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002583 # have all build states propogated to zuul?
2584 if self.haveAllBuildsReported():
2585 # Join ensures that the queue is empty _and_ events have been
2586 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002587 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002588 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002589 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002590 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002591 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002592 self.areAllNodeRequestsComplete() and
2593 all(self.eventQueuesEmpty())):
2594 # The queue empty check is placed at the end to
2595 # ensure that if a component adds an event between
2596 # when locked the run handler and checked that the
2597 # components were stable, we don't erroneously
2598 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002599 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002600 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002601 self.log.debug("...settled.")
2602 return
2603 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002604 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002605 self.sched.wake_event.wait(0.1)
2606
2607 def countJobResults(self, jobs, result):
2608 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002609 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002610
Monty Taylor0d926122017-05-24 08:07:56 -05002611 def getBuildByName(self, name):
2612 for build in self.builds:
2613 if build.name == name:
2614 return build
2615 raise Exception("Unable to find build %s" % name)
2616
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002617 def assertJobNotInHistory(self, name, project=None):
2618 for job in self.history:
2619 if (project is None or
2620 job.parameters['zuul']['project']['name'] == project):
2621 self.assertNotEqual(job.name, name,
2622 'Job %s found in history' % name)
2623
James E. Blair96c6bf82016-01-15 16:20:40 -08002624 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002625 for job in self.history:
2626 if (job.name == name and
2627 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002628 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002629 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002630 raise Exception("Unable to find job %s in history" % name)
2631
2632 def assertEmptyQueues(self):
2633 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002634 for tenant in self.sched.abide.tenants.values():
2635 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002636 for pipeline_queue in pipeline.queues:
2637 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002638 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002639 pipeline.name, pipeline_queue.name,
2640 pipeline_queue.queue))
2641 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002642 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002643
2644 def assertReportedStat(self, key, value=None, kind=None):
2645 start = time.time()
2646 while time.time() < (start + 5):
2647 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002648 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002649 if key == k:
2650 if value is None and kind is None:
2651 return
2652 elif value:
2653 if value == v:
2654 return
2655 elif kind:
2656 if v.endswith('|' + kind):
2657 return
2658 time.sleep(0.1)
2659
Clark Boylanb640e052014-04-03 16:41:46 -07002660 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002661
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002662 def assertBuilds(self, builds):
2663 """Assert that the running builds are as described.
2664
2665 The list of running builds is examined and must match exactly
2666 the list of builds described by the input.
2667
2668 :arg list builds: A list of dictionaries. Each item in the
2669 list must match the corresponding build in the build
2670 history, and each element of the dictionary must match the
2671 corresponding attribute of the build.
2672
2673 """
James E. Blair3158e282016-08-19 09:34:11 -07002674 try:
2675 self.assertEqual(len(self.builds), len(builds))
2676 for i, d in enumerate(builds):
2677 for k, v in d.items():
2678 self.assertEqual(
2679 getattr(self.builds[i], k), v,
2680 "Element %i in builds does not match" % (i,))
2681 except Exception:
2682 for build in self.builds:
2683 self.log.error("Running build: %s" % build)
2684 else:
2685 self.log.error("No running builds")
2686 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002687
James E. Blairb536ecc2016-08-31 10:11:42 -07002688 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002689 """Assert that the completed builds are as described.
2690
2691 The list of completed builds is examined and must match
2692 exactly the list of builds described by the input.
2693
2694 :arg list history: A list of dictionaries. Each item in the
2695 list must match the corresponding build in the build
2696 history, and each element of the dictionary must match the
2697 corresponding attribute of the build.
2698
James E. Blairb536ecc2016-08-31 10:11:42 -07002699 :arg bool ordered: If true, the history must match the order
2700 supplied, if false, the builds are permitted to have
2701 arrived in any order.
2702
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002703 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002704 def matches(history_item, item):
2705 for k, v in item.items():
2706 if getattr(history_item, k) != v:
2707 return False
2708 return True
James E. Blair3158e282016-08-19 09:34:11 -07002709 try:
2710 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002711 if ordered:
2712 for i, d in enumerate(history):
2713 if not matches(self.history[i], d):
2714 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002715 "Element %i in history does not match %s" %
2716 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002717 else:
2718 unseen = self.history[:]
2719 for i, d in enumerate(history):
2720 found = False
2721 for unseen_item in unseen:
2722 if matches(unseen_item, d):
2723 found = True
2724 unseen.remove(unseen_item)
2725 break
2726 if not found:
2727 raise Exception("No match found for element %i "
2728 "in history" % (i,))
2729 if unseen:
2730 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002731 except Exception:
2732 for build in self.history:
2733 self.log.error("Completed build: %s" % build)
2734 else:
2735 self.log.error("No completed builds")
2736 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002737
James E. Blair6ac368c2016-12-22 18:07:20 -08002738 def printHistory(self):
2739 """Log the build history.
2740
2741 This can be useful during tests to summarize what jobs have
2742 completed.
2743
2744 """
2745 self.log.debug("Build history:")
2746 for build in self.history:
2747 self.log.debug(build)
2748
James E. Blair59fdbac2015-12-07 17:08:06 -08002749 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002750 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2751
James E. Blair9ea70072017-04-19 16:05:30 -07002752 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002753 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002754 if not os.path.exists(root):
2755 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002756 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2757 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002758- tenant:
2759 name: openstack
2760 source:
2761 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002762 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002763 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002764 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002765 - org/project
2766 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002767 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002768 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002769 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002770 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002771 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002772
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002773 def addTagToRepo(self, project, name, sha):
2774 path = os.path.join(self.upstream_root, project)
2775 repo = git.Repo(path)
2776 repo.git.tag(name, sha)
2777
2778 def delTagFromRepo(self, project, name):
2779 path = os.path.join(self.upstream_root, project)
2780 repo = git.Repo(path)
2781 repo.git.tag('-d', name)
2782
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002783 def addCommitToRepo(self, project, message, files,
2784 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002785 path = os.path.join(self.upstream_root, project)
2786 repo = git.Repo(path)
2787 repo.head.reference = branch
2788 zuul.merger.merger.reset_repo_to_head(repo)
2789 for fn, content in files.items():
2790 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002791 try:
2792 os.makedirs(os.path.dirname(fn))
2793 except OSError:
2794 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002795 with open(fn, 'w') as f:
2796 f.write(content)
2797 repo.index.add([fn])
2798 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002799 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002800 repo.heads[branch].commit = commit
2801 repo.head.reference = branch
2802 repo.git.clean('-x', '-f', '-d')
2803 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002804 if tag:
2805 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002806 return before
2807
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002808 def commitConfigUpdate(self, project_name, source_name):
2809 """Commit an update to zuul.yaml
2810
2811 This overwrites the zuul.yaml in the specificed project with
2812 the contents specified.
2813
2814 :arg str project_name: The name of the project containing
2815 zuul.yaml (e.g., common-config)
2816
2817 :arg str source_name: The path to the file (underneath the
2818 test fixture directory) whose contents should be used to
2819 replace zuul.yaml.
2820 """
2821
2822 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002823 files = {}
2824 with open(source_path, 'r') as f:
2825 data = f.read()
2826 layout = yaml.safe_load(data)
2827 files['zuul.yaml'] = data
2828 for item in layout:
2829 if 'job' in item:
2830 jobname = item['job']['name']
2831 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002832 before = self.addCommitToRepo(
2833 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002834 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002835 return before
2836
Clint Byrum627ba362017-08-14 13:20:40 -07002837 def newTenantConfig(self, source_name):
2838 """ Use this to update the tenant config file in tests
2839
2840 This will update self.tenant_config_file to point to a temporary file
2841 for the duration of this particular test. The content of that file will
2842 be taken from FIXTURE_DIR/source_name
2843
2844 After the test the original value of self.tenant_config_file will be
2845 restored.
2846
2847 :arg str source_name: The path of the file under
2848 FIXTURE_DIR that will be used to populate the new tenant
2849 config file.
2850 """
2851 source_path = os.path.join(FIXTURE_DIR, source_name)
2852 orig_tenant_config_file = self.tenant_config_file
2853 with tempfile.NamedTemporaryFile(
2854 delete=False, mode='wb') as new_tenant_config:
2855 self.tenant_config_file = new_tenant_config.name
2856 with open(source_path, mode='rb') as source_tenant_config:
2857 new_tenant_config.write(source_tenant_config.read())
2858 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2859 self.setupAllProjectKeys()
2860 self.log.debug(
2861 'tenant_config_file = {}'.format(self.tenant_config_file))
2862
2863 def _restoreTenantConfig():
2864 self.log.debug(
2865 'restoring tenant_config_file = {}'.format(
2866 orig_tenant_config_file))
2867 os.unlink(self.tenant_config_file)
2868 self.tenant_config_file = orig_tenant_config_file
2869 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2870 self.addCleanup(_restoreTenantConfig)
2871
James E. Blair7fc8daa2016-08-08 15:37:15 -07002872 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002873
James E. Blair7fc8daa2016-08-08 15:37:15 -07002874 """Inject a Fake (Gerrit) event.
2875
2876 This method accepts a JSON-encoded event and simulates Zuul
2877 having received it from Gerrit. It could (and should)
2878 eventually apply to any connection type, but is currently only
2879 used with Gerrit connections. The name of the connection is
2880 used to look up the corresponding server, and the event is
2881 simulated as having been received by all Zuul connections
2882 attached to that server. So if two Gerrit connections in Zuul
2883 are connected to the same Gerrit server, and you invoke this
2884 method specifying the name of one of them, the event will be
2885 received by both.
2886
2887 .. note::
2888
2889 "self.fake_gerrit.addEvent" calls should be migrated to
2890 this method.
2891
2892 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002893 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002894 :arg str event: The JSON-encoded event.
2895
2896 """
2897 specified_conn = self.connections.connections[connection]
2898 for conn in self.connections.connections.values():
2899 if (isinstance(conn, specified_conn.__class__) and
2900 specified_conn.server == conn.server):
2901 conn.addEvent(event)
2902
James E. Blaird8af5422017-05-24 13:59:40 -07002903 def getUpstreamRepos(self, projects):
2904 """Return upstream git repo objects for the listed projects
2905
2906 :arg list projects: A list of strings, each the canonical name
2907 of a project.
2908
2909 :returns: A dictionary of {name: repo} for every listed
2910 project.
2911 :rtype: dict
2912
2913 """
2914
2915 repos = {}
2916 for project in projects:
2917 # FIXME(jeblair): the upstream root does not yet have a
2918 # hostname component; that needs to be added, and this
2919 # line removed:
2920 tmp_project_name = '/'.join(project.split('/')[1:])
2921 path = os.path.join(self.upstream_root, tmp_project_name)
2922 repo = git.Repo(path)
2923 repos[project] = repo
2924 return repos
2925
James E. Blair3f876d52016-07-22 13:07:14 -07002926
2927class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002928 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002929 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002930
Jamie Lennox7655b552017-03-17 12:33:38 +11002931 @contextmanager
2932 def jobLog(self, build):
2933 """Print job logs on assertion errors
2934
2935 This method is a context manager which, if it encounters an
2936 ecxeption, adds the build log to the debug output.
2937
2938 :arg Build build: The build that's being asserted.
2939 """
2940 try:
2941 yield
2942 except Exception:
2943 path = os.path.join(self.test_root, build.uuid,
2944 'work', 'logs', 'job-output.txt')
2945 with open(path) as f:
2946 self.log.debug(f.read())
2947 raise
2948
Joshua Heskethd78b4482015-09-14 16:56:34 -06002949
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002950class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002951 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002952 use_ssl = True
2953
2954
Joshua Heskethd78b4482015-09-14 16:56:34 -06002955class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002956 def setup_config(self):
2957 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002958 for section_name in self.config.sections():
2959 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2960 section_name, re.I)
2961 if not con_match:
2962 continue
2963
2964 if self.config.get(section_name, 'driver') == 'sql':
2965 f = MySQLSchemaFixture()
2966 self.useFixture(f)
2967 if (self.config.get(section_name, 'dburi') ==
2968 '$MYSQL_FIXTURE_DBURI$'):
2969 self.config.set(section_name, 'dburi', f.dburi)