blob: 0f2df353fcba806ad04d07a44efaff6a28ecf030 [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
Paul Belanger174a8272017-03-14 13:20:10 -040060import zuul.executor.server
61import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020066import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070067import zuul.nodepool
Jesse Keating80730e62017-09-14 15:35:11 -060068import zuul.rpcclient
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Jesse Keating80730e62017-09-14 15:35:11 -060072from zuul.lib.config import get_default
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200136 categories = {'Approved': ('Approved', -1, 1),
137 'Code-Review': ('Code-Review', -2, 2),
138 'Verified': ('Verified', -2, 2)}
139
Clark Boylanb640e052014-04-03 16:41:46 -0700140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700141 status='NEW', upstream_root=None, files={},
142 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800174 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700175
176 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700177 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair289f5932017-07-27 15:02:29 -0700181 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700184 if parent is None:
185 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700188 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700197 if content is None:
198 os.unlink(fn)
199 repo.index.remove([fn])
200 else:
201 d = os.path.dirname(fn)
202 if not os.path.exists(d):
203 os.makedirs(d)
204 with open(fn, 'w') as f:
205 f.write(content)
206 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700207 else:
208 for fni in range(100):
209 fn = os.path.join(path, str(fni))
210 f = open(fn, 'w')
211 for ci in range(4096):
212 f.write(random.choice(string.printable))
213 f.close()
214 repo.index.add([fn])
215
216 r = repo.index.commit(msg)
217 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700218 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700219 repo.git.clean('-x', '-f', '-d')
220 repo.heads['master'].checkout()
221 return r
222
James E. Blair289f5932017-07-27 15:02:29 -0700223 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700224 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700225 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700226 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 data = ("test %s %s %s\n" %
228 (self.branch, self.number, self.latest_patchset))
229 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700230 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700231 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700232 ps_files = [{'file': '/COMMIT_MSG',
233 'type': 'ADDED'},
234 {'file': 'README',
235 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700236 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700237 ps_files.append({'file': f, 'type': 'ADDED'})
238 d = {'approvals': [],
239 'createdOn': time.time(),
240 'files': ps_files,
241 'number': str(self.latest_patchset),
242 'ref': 'refs/changes/1/%s/%s' % (self.number,
243 self.latest_patchset),
244 'revision': c.hexsha,
245 'uploader': {'email': 'user@example.com',
246 'name': 'User name',
247 'username': 'user'}}
248 self.data['currentPatchSet'] = d
249 self.patchsets.append(d)
250 self.data['submitRecords'] = self.getSubmitRecords()
251
252 def getPatchsetCreatedEvent(self, patchset):
253 event = {"type": "patchset-created",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "uploader": {"name": "User Name"}}
263 return event
264
265 def getChangeRestoredEvent(self):
266 event = {"type": "change-restored",
267 "change": {"project": self.project,
268 "branch": self.branch,
269 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
270 "number": str(self.number),
271 "subject": self.subject,
272 "owner": {"name": "User Name"},
273 "url": "https://hostname/3"},
274 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100275 "patchSet": self.patchsets[-1],
276 "reason": ""}
277 return event
278
279 def getChangeAbandonedEvent(self):
280 event = {"type": "change-abandoned",
281 "change": {"project": self.project,
282 "branch": self.branch,
283 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
284 "number": str(self.number),
285 "subject": self.subject,
286 "owner": {"name": "User Name"},
287 "url": "https://hostname/3"},
288 "abandoner": {"name": "User Name"},
289 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700290 "reason": ""}
291 return event
292
293 def getChangeCommentEvent(self, patchset):
294 event = {"type": "comment-added",
295 "change": {"project": self.project,
296 "branch": self.branch,
297 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
298 "number": str(self.number),
299 "subject": self.subject,
300 "owner": {"name": "User Name"},
301 "url": "https://hostname/3"},
302 "patchSet": self.patchsets[patchset - 1],
303 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200304 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700305 "description": "Code-Review",
306 "value": "0"}],
307 "comment": "This is a comment"}
308 return event
309
James E. Blairc2a5ed72017-02-20 14:12:01 -0500310 def getChangeMergedEvent(self):
311 event = {"submitter": {"name": "Jenkins",
312 "username": "jenkins"},
313 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
314 "patchSet": self.patchsets[-1],
315 "change": self.data,
316 "type": "change-merged",
317 "eventCreatedOn": 1487613810}
318 return event
319
James E. Blair8cce42e2016-10-18 08:18:36 -0700320 def getRefUpdatedEvent(self):
321 path = os.path.join(self.upstream_root, self.project)
322 repo = git.Repo(path)
323 oldrev = repo.heads[self.branch].commit.hexsha
324
325 event = {
326 "type": "ref-updated",
327 "submitter": {
328 "name": "User Name",
329 },
330 "refUpdate": {
331 "oldRev": oldrev,
332 "newRev": self.patchsets[-1]['revision'],
333 "refName": self.branch,
334 "project": self.project,
335 }
336 }
337 return event
338
Joshua Hesketh642824b2014-07-01 17:54:59 +1000339 def addApproval(self, category, value, username='reviewer_john',
340 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700341 if not granted_on:
342 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000343 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200344 'description': self.categories[category][0],
345 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000346 'value': str(value),
347 'by': {
348 'username': username,
349 'email': username + '@example.com',
350 },
351 'grantedOn': int(granted_on)
352 }
Clark Boylanb640e052014-04-03 16:41:46 -0700353 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200354 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700355 del self.patchsets[-1]['approvals'][i]
356 self.patchsets[-1]['approvals'].append(approval)
357 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'author': {'email': 'author@example.com',
359 'name': 'Patchset Author',
360 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700361 'change': {'branch': self.branch,
362 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
363 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'owner': {'email': 'owner@example.com',
365 'name': 'Change Owner',
366 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700367 'project': self.project,
368 'subject': self.subject,
369 'topic': 'master',
370 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000371 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700372 'patchSet': self.patchsets[-1],
373 'type': 'comment-added'}
374 self.data['submitRecords'] = self.getSubmitRecords()
375 return json.loads(json.dumps(event))
376
377 def getSubmitRecords(self):
378 status = {}
379 for cat in self.categories.keys():
380 status[cat] = 0
381
382 for a in self.patchsets[-1]['approvals']:
383 cur = status[a['type']]
384 cat_min, cat_max = self.categories[a['type']][1:]
385 new = int(a['value'])
386 if new == cat_min:
387 cur = new
388 elif abs(new) > abs(cur):
389 cur = new
390 status[a['type']] = cur
391
392 labels = []
393 ok = True
394 for typ, cat in self.categories.items():
395 cur = status[typ]
396 cat_min, cat_max = cat[1:]
397 if cur == cat_min:
398 value = 'REJECT'
399 ok = False
400 elif cur == cat_max:
401 value = 'OK'
402 else:
403 value = 'NEED'
404 ok = False
405 labels.append({'label': cat[0], 'status': value})
406 if ok:
407 return [{'status': 'OK'}]
408 return [{'status': 'NOT_READY',
409 'labels': labels}]
410
411 def setDependsOn(self, other, patchset):
412 self.depends_on_change = other
413 d = {'id': other.data['id'],
414 'number': other.data['number'],
415 'ref': other.patchsets[patchset - 1]['ref']
416 }
417 self.data['dependsOn'] = [d]
418
419 other.needed_by_changes.append(self)
420 needed = other.data.get('neededBy', [])
421 d = {'id': self.data['id'],
422 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700423 'ref': self.patchsets[-1]['ref'],
424 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700425 }
426 needed.append(d)
427 other.data['neededBy'] = needed
428
429 def query(self):
430 self.queried += 1
431 d = self.data.get('dependsOn')
432 if d:
433 d = d[0]
434 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
435 d['isCurrentPatchSet'] = True
436 else:
437 d['isCurrentPatchSet'] = False
438 return json.loads(json.dumps(self.data))
439
440 def setMerged(self):
441 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000442 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700443 return
444 if self.fail_merge:
445 return
446 self.data['status'] = 'MERGED'
447 self.open = False
448
449 path = os.path.join(self.upstream_root, self.project)
450 repo = git.Repo(path)
451 repo.heads[self.branch].commit = \
452 repo.commit(self.patchsets[-1]['revision'])
453
454 def setReported(self):
455 self.reported += 1
456
457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700459 """A Fake Gerrit connection for use in tests.
460
461 This subclasses
462 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
463 ability for tests to add changes to the fake Gerrit it represents.
464 """
465
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700467
James E. Blaire511d2f2016-12-08 15:22:26 -0800468 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700469 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800470 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000471 connection_config)
472
Monty Taylorb934c1a2017-06-16 19:31:47 -0500473 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
475 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000476 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700477 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200478 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700479
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700480 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700481 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700482 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700483 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700484 c = FakeGerritChange(self, self.change_number, project, branch,
485 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700486 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700487 self.changes[self.change_number] = c
488 return c
489
James E. Blair1edfd972017-12-01 15:54:24 -0800490 def addFakeTag(self, project, branch, tag):
491 path = os.path.join(self.upstream_root, project)
492 repo = git.Repo(path)
493 commit = repo.heads[branch].commit
494 newrev = commit.hexsha
495 ref = 'refs/tags/' + tag
496
497 git.Tag.create(repo, tag, commit)
498
499 event = {
500 "type": "ref-updated",
501 "submitter": {
502 "name": "User Name",
503 },
504 "refUpdate": {
505 "oldRev": 40 * '0',
506 "newRev": newrev,
507 "refName": ref,
508 "project": project,
509 }
510 }
511 return event
512
James E. Blair72facdc2017-08-17 10:29:12 -0700513 def getFakeBranchCreatedEvent(self, project, branch):
514 path = os.path.join(self.upstream_root, project)
515 repo = git.Repo(path)
516 oldrev = 40 * '0'
517
518 event = {
519 "type": "ref-updated",
520 "submitter": {
521 "name": "User Name",
522 },
523 "refUpdate": {
524 "oldRev": oldrev,
525 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700526 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700527 "project": project,
528 }
529 }
530 return event
531
Clark Boylanb640e052014-04-03 16:41:46 -0700532 def review(self, project, changeid, message, action):
533 number, ps = changeid.split(',')
534 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000535
536 # Add the approval back onto the change (ie simulate what gerrit would
537 # do).
538 # Usually when zuul leaves a review it'll create a feedback loop where
539 # zuul's review enters another gerrit event (which is then picked up by
540 # zuul). However, we can't mimic this behaviour (by adding this
541 # approval event into the queue) as it stops jobs from checking what
542 # happens before this event is triggered. If a job needs to see what
543 # happens they can add their own verified event into the queue.
544 # Nevertheless, we can update change with the new review in gerrit.
545
James E. Blair8b5408c2016-08-08 15:37:46 -0700546 for cat in action.keys():
547 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000548 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000549
Clark Boylanb640e052014-04-03 16:41:46 -0700550 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000551
Clark Boylanb640e052014-04-03 16:41:46 -0700552 if 'submit' in action:
553 change.setMerged()
554 if message:
555 change.setReported()
556
557 def query(self, number):
558 change = self.changes.get(int(number))
559 if change:
560 return change.query()
561 return {}
562
James E. Blair0e4c7912018-01-02 14:20:17 -0800563 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800564 if query.startswith('change:'):
565 # Query a specific changeid
566 changeid = query[len('change:'):]
567 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800568 if (change.data['id'] == changeid or
569 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700570 elif query.startswith('message:'):
571 # Query the content of a commit message
572 msg = query[len('message:'):].strip()
573 l = [change.query() for change in self.changes.values()
574 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800575 else:
576 # Query all open changes
577 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700578 return l
James E. Blairc494d542014-08-06 09:23:52 -0700579
James E. Blair0e4c7912018-01-02 14:20:17 -0800580 def simpleQuery(self, query):
581 self.log.debug("simpleQuery: %s" % query)
582 self.queries.append(query)
583 results = []
584 if query.startswith('(') and 'OR' in query:
585 query = query[1:-2]
586 for q in query.split(' OR '):
587 for r in self._simpleQuery(q):
588 if r not in results:
589 results.append(r)
590 else:
591 results = self._simpleQuery(query)
592 return results
593
Joshua Hesketh352264b2015-08-11 23:42:08 +1000594 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700595 pass
596
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200597 def _uploadPack(self, project):
598 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
599 'multi_ack thin-pack side-band side-band-64k ofs-delta '
600 'shallow no-progress include-tag multi_ack_detailed no-done\n')
601 path = os.path.join(self.upstream_root, project.name)
602 repo = git.Repo(path)
603 for ref in repo.refs:
604 r = ref.object.hexsha + ' ' + ref.path + '\n'
605 ret += '%04x%s' % (len(r) + 4, r)
606 ret += '0000'
607 return ret
608
Joshua Hesketh352264b2015-08-11 23:42:08 +1000609 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800610 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000611
Clark Boylanb640e052014-04-03 16:41:46 -0700612
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613class GithubChangeReference(git.Reference):
614 _common_path_default = "refs/pull"
615 _points_to_commits_only = True
616
617
618class FakeGithubPullRequest(object):
619
620 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800621 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700622 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700623 """Creates a new PR with several commits.
624 Sends an event about opened PR."""
625 self.github = github
626 self.source = github
627 self.number = number
628 self.project = project
629 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100630 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700631 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100632 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700633 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100634 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700635 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100636 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100637 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800638 self.reviews = []
639 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700640 self.updated_at = None
641 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100642 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100643 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700644 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800645 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700646 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100647 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700648 self._updateTimeStamp()
649
Jan Hruban570d01c2016-03-10 21:51:32 +0100650 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700651 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100652 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700653 self._updateTimeStamp()
654
Jan Hruban570d01c2016-03-10 21:51:32 +0100655 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700656 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100657 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700658 self._updateTimeStamp()
659
660 def getPullRequestOpenedEvent(self):
661 return self._getPullRequestEvent('opened')
662
663 def getPullRequestSynchronizeEvent(self):
664 return self._getPullRequestEvent('synchronize')
665
666 def getPullRequestReopenedEvent(self):
667 return self._getPullRequestEvent('reopened')
668
669 def getPullRequestClosedEvent(self):
670 return self._getPullRequestEvent('closed')
671
Jesse Keatinga41566f2017-06-14 18:17:51 -0700672 def getPullRequestEditedEvent(self):
673 return self._getPullRequestEvent('edited')
674
Gregory Haynes4fc12542015-04-22 20:38:06 -0700675 def addComment(self, message):
676 self.comments.append(message)
677 self._updateTimeStamp()
678
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200679 def getCommentAddedEvent(self, text):
680 name = 'issue_comment'
681 data = {
682 'action': 'created',
683 'issue': {
684 'number': self.number
685 },
686 'comment': {
687 'body': text
688 },
689 'repository': {
690 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100691 },
692 'sender': {
693 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200694 }
695 }
696 return (name, data)
697
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800698 def getReviewAddedEvent(self, review):
699 name = 'pull_request_review'
700 data = {
701 'action': 'submitted',
702 'pull_request': {
703 'number': self.number,
704 'title': self.subject,
705 'updated_at': self.updated_at,
706 'base': {
707 'ref': self.branch,
708 'repo': {
709 'full_name': self.project
710 }
711 },
712 'head': {
713 'sha': self.head_sha
714 }
715 },
716 'review': {
717 'state': review
718 },
719 'repository': {
720 'full_name': self.project
721 },
722 'sender': {
723 'login': 'ghuser'
724 }
725 }
726 return (name, data)
727
Jan Hruban16ad31f2015-11-07 14:39:07 +0100728 def addLabel(self, name):
729 if name not in self.labels:
730 self.labels.append(name)
731 self._updateTimeStamp()
732 return self._getLabelEvent(name)
733
734 def removeLabel(self, name):
735 if name in self.labels:
736 self.labels.remove(name)
737 self._updateTimeStamp()
738 return self._getUnlabelEvent(name)
739
740 def _getLabelEvent(self, label):
741 name = 'pull_request'
742 data = {
743 'action': 'labeled',
744 'pull_request': {
745 'number': self.number,
746 'updated_at': self.updated_at,
747 'base': {
748 'ref': self.branch,
749 'repo': {
750 'full_name': self.project
751 }
752 },
753 'head': {
754 'sha': self.head_sha
755 }
756 },
757 'label': {
758 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100759 },
760 'sender': {
761 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100762 }
763 }
764 return (name, data)
765
766 def _getUnlabelEvent(self, label):
767 name = 'pull_request'
768 data = {
769 'action': 'unlabeled',
770 'pull_request': {
771 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100772 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100773 'updated_at': self.updated_at,
774 'base': {
775 'ref': self.branch,
776 'repo': {
777 'full_name': self.project
778 }
779 },
780 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800781 'sha': self.head_sha,
782 'repo': {
783 'full_name': self.project
784 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100785 }
786 },
787 'label': {
788 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100789 },
790 'sender': {
791 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100792 }
793 }
794 return (name, data)
795
Jesse Keatinga41566f2017-06-14 18:17:51 -0700796 def editBody(self, body):
797 self.body = body
798 self._updateTimeStamp()
799
Gregory Haynes4fc12542015-04-22 20:38:06 -0700800 def _getRepo(self):
801 repo_path = os.path.join(self.upstream_root, self.project)
802 return git.Repo(repo_path)
803
804 def _createPRRef(self):
805 repo = self._getRepo()
806 GithubChangeReference.create(
807 repo, self._getPRReference(), 'refs/tags/init')
808
Jan Hruban570d01c2016-03-10 21:51:32 +0100809 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 repo = self._getRepo()
811 ref = repo.references[self._getPRReference()]
812 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100813 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100815 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 repo.head.reference = ref
817 zuul.merger.merger.reset_repo_to_head(repo)
818 repo.git.clean('-x', '-f', '-d')
819
Jan Hruban570d01c2016-03-10 21:51:32 +0100820 if files:
821 fn = files[0]
822 self.files = files
823 else:
824 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
825 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100826 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700827 fn = os.path.join(repo.working_dir, fn)
828 f = open(fn, 'w')
829 with open(fn, 'w') as f:
830 f.write("test %s %s\n" %
831 (self.branch, self.number))
832 repo.index.add([fn])
833
834 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800835 # Create an empty set of statuses for the given sha,
836 # each sha on a PR may have a status set on it
837 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 repo.head.reference = 'master'
839 zuul.merger.merger.reset_repo_to_head(repo)
840 repo.git.clean('-x', '-f', '-d')
841 repo.heads['master'].checkout()
842
843 def _updateTimeStamp(self):
844 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
845
846 def getPRHeadSha(self):
847 repo = self._getRepo()
848 return repo.references[self._getPRReference()].commit.hexsha
849
Jesse Keatingae4cd272017-01-30 17:10:44 -0800850 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800851 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
852 # convert the timestamp to a str format that would be returned
853 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800854
Adam Gandelmand81dd762017-02-09 15:15:49 -0800855 if granted_on:
856 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
857 submitted_at = time.strftime(
858 gh_time_format, granted_on.timetuple())
859 else:
860 # github timestamps only down to the second, so we need to make
861 # sure reviews that tests add appear to be added over a period of
862 # time in the past and not all at once.
863 if not self.reviews:
864 # the first review happens 10 mins ago
865 offset = 600
866 else:
867 # subsequent reviews happen 1 minute closer to now
868 offset = 600 - (len(self.reviews) * 60)
869
870 granted_on = datetime.datetime.utcfromtimestamp(
871 time.time() - offset)
872 submitted_at = time.strftime(
873 gh_time_format, granted_on.timetuple())
874
Jesse Keatingae4cd272017-01-30 17:10:44 -0800875 self.reviews.append({
876 'state': state,
877 'user': {
878 'login': user,
879 'email': user + "@derp.com",
880 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800881 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800882 })
883
Gregory Haynes4fc12542015-04-22 20:38:06 -0700884 def _getPRReference(self):
885 return '%s/head' % self.number
886
887 def _getPullRequestEvent(self, action):
888 name = 'pull_request'
889 data = {
890 'action': action,
891 'number': self.number,
892 'pull_request': {
893 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100894 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700895 'updated_at': self.updated_at,
896 'base': {
897 'ref': self.branch,
898 'repo': {
899 'full_name': self.project
900 }
901 },
902 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800903 'sha': self.head_sha,
904 'repo': {
905 'full_name': self.project
906 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700907 },
908 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100909 },
910 'sender': {
911 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 }
913 }
914 return (name, data)
915
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800916 def getCommitStatusEvent(self, context, state='success', user='zuul'):
917 name = 'status'
918 data = {
919 'state': state,
920 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700921 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800922 'description': 'Test results for %s: %s' % (self.head_sha, state),
923 'target_url': 'http://zuul/%s' % self.head_sha,
924 'branches': [],
925 'context': context,
926 'sender': {
927 'login': user
928 }
929 }
930 return (name, data)
931
James E. Blair289f5932017-07-27 15:02:29 -0700932 def setMerged(self, commit_message):
933 self.is_merged = True
934 self.merge_message = commit_message
935
936 repo = self._getRepo()
937 repo.heads[self.branch].commit = repo.commit(self.head_sha)
938
Gregory Haynes4fc12542015-04-22 20:38:06 -0700939
940class FakeGithubConnection(githubconnection.GithubConnection):
941 log = logging.getLogger("zuul.test.FakeGithubConnection")
942
Jesse Keating80730e62017-09-14 15:35:11 -0600943 def __init__(self, driver, connection_name, connection_config, rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -0800944 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700945 super(FakeGithubConnection, self).__init__(driver, connection_name,
946 connection_config)
947 self.connection_name = connection_name
948 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800949 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700950 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700951 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100952 self.merge_failure = False
953 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100954 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800955 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Jesse Keating80730e62017-09-14 15:35:11 -0600956 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200957
958 def getGithubClient(self,
959 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600960 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200961 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700962
Jesse Keating80730e62017-09-14 15:35:11 -0600963 def setZuulWebPort(self, port):
964 self.zuul_web_port = port
965
Jesse Keatinga41566f2017-06-14 18:17:51 -0700966 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700967 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 self.pr_number += 1
969 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100970 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700971 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800972 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700973 return pull_request
974
Jesse Keating71a47ff2017-06-06 11:36:43 -0700975 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
976 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700977 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700978 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700979 if not new_rev:
980 new_rev = random_sha1()
981 name = 'push'
982 data = {
983 'ref': ref,
984 'before': old_rev,
985 'after': new_rev,
986 'repository': {
987 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700988 },
989 'commits': [
990 {
991 'added': added_files,
992 'removed': removed_files,
993 'modified': modified_files
994 }
995 ]
Wayne1a78c612015-06-11 17:14:13 -0700996 }
997 return (name, data)
998
Jesse Keating80730e62017-09-14 15:35:11 -0600999 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001000 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001002 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001003 secret = self.connection_config['webhook_token']
1004 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001005 headers = {'x-github-event': name, 'x-hub-signature': signature}
1006
1007 if use_zuulweb:
1008 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001009 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001010 % (self.zuul_web_port, self.connection_name),
1011 data=payload, headers=headers)
1012 return urllib.request.urlopen(req)
1013 else:
1014 job = self.rpcclient.submitJob(
1015 'github:%s:payload' % self.connection_name,
1016 {'headers': headers, 'body': data})
1017 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001019 def addProject(self, project):
1020 # use the original method here and additionally register it in the
1021 # fake github
1022 super(FakeGithubConnection, self).addProject(project)
1023 self.getGithubClient(project).addProject(project)
1024
Jesse Keating9021a012017-08-29 14:45:27 -07001025 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001026 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001027 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001028 if len(prs) > 1:
1029 raise Exception('Multiple pulls found with head sha: %s' % sha)
1030 pr = prs[0]
1031 return self.getPull(pr.project, pr.number)
1032
Jesse Keatingae4cd272017-01-30 17:10:44 -08001033 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001034 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001035 return pr.reviews
1036
Jesse Keatingae4cd272017-01-30 17:10:44 -08001037 def getRepoPermission(self, project, login):
1038 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001039 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001040 pr_owner, pr_project = pr.project.split('/')
1041 if (pr_owner == owner and proj == pr_project):
1042 if login in pr.writers:
1043 return 'write'
1044 else:
1045 return 'read'
1046
Gregory Haynes4fc12542015-04-22 20:38:06 -07001047 def getGitUrl(self, project):
1048 return os.path.join(self.upstream_root, str(project))
1049
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001050 def real_getGitUrl(self, project):
1051 return super(FakeGithubConnection, self).getGitUrl(project)
1052
Jan Hrubane252a732017-01-03 15:03:09 +01001053 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001054 # record that this got reported
1055 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001056 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001057 pull_request.addComment(message)
1058
Jan Hruban3b415922016-02-03 13:10:22 +01001059 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001060 # record that this got reported
1061 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001062 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001063 if self.merge_failure:
1064 raise Exception('Pull request was not merged')
1065 if self.merge_not_allowed_count > 0:
1066 self.merge_not_allowed_count -= 1
1067 raise MergeFailure('Merge was not successful due to mergeability'
1068 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001069 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001070
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001071 def setCommitStatus(self, project, sha, state, url='', description='',
1072 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001073 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001074 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001075 super(FakeGithubConnection, self).setCommitStatus(
1076 project, sha, state,
1077 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001078
Jan Hruban16ad31f2015-11-07 14:39:07 +01001079 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001080 # record that this got reported
1081 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001082 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001083 pull_request.addLabel(label)
1084
1085 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001086 # record that this got reported
1087 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001088 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001089 pull_request.removeLabel(label)
1090
Gregory Haynes4fc12542015-04-22 20:38:06 -07001091
Clark Boylanb640e052014-04-03 16:41:46 -07001092class BuildHistory(object):
1093 def __init__(self, **kw):
1094 self.__dict__.update(kw)
1095
1096 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001097 return ("<Completed build, result: %s name: %s uuid: %s "
1098 "changes: %s ref: %s>" %
1099 (self.result, self.name, self.uuid,
1100 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001101
1102
Clark Boylanb640e052014-04-03 16:41:46 -07001103class FakeStatsd(threading.Thread):
1104 def __init__(self):
1105 threading.Thread.__init__(self)
1106 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001107 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001108 self.sock.bind(('', 0))
1109 self.port = self.sock.getsockname()[1]
1110 self.wake_read, self.wake_write = os.pipe()
1111 self.stats = []
1112
1113 def run(self):
1114 while True:
1115 poll = select.poll()
1116 poll.register(self.sock, select.POLLIN)
1117 poll.register(self.wake_read, select.POLLIN)
1118 ret = poll.poll()
1119 for (fd, event) in ret:
1120 if fd == self.sock.fileno():
1121 data = self.sock.recvfrom(1024)
1122 if not data:
1123 return
1124 self.stats.append(data[0])
1125 if fd == self.wake_read:
1126 return
1127
1128 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001129 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001130
1131
James E. Blaire1767bc2016-08-02 10:00:27 -07001132class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 log = logging.getLogger("zuul.test")
1134
Paul Belanger174a8272017-03-14 13:20:10 -04001135 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001137 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001139 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001140 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001142 # TODOv3(jeblair): self.node is really "the label of the node
1143 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001144 # keep using it like this, or we may end up exposing more of
1145 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001146 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001147 self.node = None
1148 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001149 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001150 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001151 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001152 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001153 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.wait_condition = threading.Condition()
1155 self.waiting = False
1156 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001157 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001158 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001159 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001160 items = self.parameters['zuul']['items']
1161 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1162 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001163
James E. Blair3158e282016-08-19 09:34:11 -07001164 def __repr__(self):
1165 waiting = ''
1166 if self.waiting:
1167 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001168 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1169 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001170
Clark Boylanb640e052014-04-03 16:41:46 -07001171 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001172 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.wait_condition.acquire()
1174 self.wait_condition.notify()
1175 self.waiting = False
1176 self.log.debug("Build %s released" % self.unique)
1177 self.wait_condition.release()
1178
1179 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001180 """Return whether this build is being held.
1181
1182 :returns: Whether the build is being held.
1183 :rtype: bool
1184 """
1185
Clark Boylanb640e052014-04-03 16:41:46 -07001186 self.wait_condition.acquire()
1187 if self.waiting:
1188 ret = True
1189 else:
1190 ret = False
1191 self.wait_condition.release()
1192 return ret
1193
1194 def _wait(self):
1195 self.wait_condition.acquire()
1196 self.waiting = True
1197 self.log.debug("Build %s waiting" % self.unique)
1198 self.wait_condition.wait()
1199 self.wait_condition.release()
1200
1201 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001202 self.log.debug('Running build %s' % self.unique)
1203
Paul Belanger174a8272017-03-14 13:20:10 -04001204 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.log.debug('Holding build %s' % self.unique)
1206 self._wait()
1207 self.log.debug("Build %s continuing" % self.unique)
1208
James E. Blair412fba82017-01-26 15:00:50 -08001209 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001210 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001211 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001212 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001213 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001214 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001215 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001216
James E. Blaire1767bc2016-08-02 10:00:27 -07001217 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001218
James E. Blaira5dba232016-08-08 15:53:24 -07001219 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001220 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001221 for change in changes:
1222 if self.hasChanges(change):
1223 return True
1224 return False
1225
James E. Blaire7b99a02016-08-05 14:27:34 -07001226 def hasChanges(self, *changes):
1227 """Return whether this build has certain changes in its git repos.
1228
1229 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001230 are expected to be present (in order) in the git repository of
1231 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001232
1233 :returns: Whether the build has the indicated changes.
1234 :rtype: bool
1235
1236 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001237 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001238 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001239 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001240 try:
1241 repo = git.Repo(path)
1242 except NoSuchPathError as e:
1243 self.log.debug('%s' % e)
1244 return False
James E. Blair247cab72017-07-20 16:52:36 -07001245 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001246 commit_message = '%s-1' % change.subject
1247 self.log.debug("Checking if build %s has changes; commit_message "
1248 "%s; repo_messages %s" % (self, commit_message,
1249 repo_messages))
1250 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001251 self.log.debug(" messages do not match")
1252 return False
1253 self.log.debug(" OK")
1254 return True
1255
James E. Blaird8af5422017-05-24 13:59:40 -07001256 def getWorkspaceRepos(self, projects):
1257 """Return workspace git repo objects for the listed projects
1258
1259 :arg list projects: A list of strings, each the canonical name
1260 of a project.
1261
1262 :returns: A dictionary of {name: repo} for every listed
1263 project.
1264 :rtype: dict
1265
1266 """
1267
1268 repos = {}
1269 for project in projects:
1270 path = os.path.join(self.jobdir.src_root, project)
1271 repo = git.Repo(path)
1272 repos[project] = repo
1273 return repos
1274
Clark Boylanb640e052014-04-03 16:41:46 -07001275
James E. Blair107bb252017-10-13 15:53:16 -07001276class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1277 def doMergeChanges(self, merger, items, repo_state):
1278 # Get a merger in order to update the repos involved in this job.
1279 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1280 merger, items, repo_state)
1281 if not commit: # merge conflict
1282 self.recordResult('MERGER_FAILURE')
1283 return commit
1284
1285 def recordResult(self, result):
1286 build = self.executor_server.job_builds[self.job.unique]
1287 self.executor_server.lock.acquire()
1288 self.executor_server.build_history.append(
1289 BuildHistory(name=build.name, result=result, changes=build.changes,
1290 node=build.node, uuid=build.unique,
1291 ref=build.parameters['zuul']['ref'],
1292 parameters=build.parameters, jobdir=build.jobdir,
1293 pipeline=build.parameters['zuul']['pipeline'])
1294 )
1295 self.executor_server.running_builds.remove(build)
1296 del self.executor_server.job_builds[self.job.unique]
1297 self.executor_server.lock.release()
1298
1299 def runPlaybooks(self, args):
1300 build = self.executor_server.job_builds[self.job.unique]
1301 build.jobdir = self.jobdir
1302
1303 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1304 self.recordResult(result)
1305 return result
1306
James E. Blaira86aaf12017-10-15 20:59:50 -07001307 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001308 build = self.executor_server.job_builds[self.job.unique]
1309
1310 if self.executor_server._run_ansible:
1311 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001312 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001313 else:
1314 if playbook.path:
1315 result = build.run()
1316 else:
1317 result = (self.RESULT_NORMAL, 0)
1318 return result
1319
1320 def getHostList(self, args):
1321 self.log.debug("hostlist")
1322 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1323 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001324 if not host['host_vars'].get('ansible_connection'):
1325 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001326
1327 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001328 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001329 host_vars=dict(ansible_connection='local'),
1330 host_keys=[]))
1331 return hosts
1332
1333
Paul Belanger174a8272017-03-14 13:20:10 -04001334class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1335 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001336
Paul Belanger174a8272017-03-14 13:20:10 -04001337 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001338 they will report that they have started but then pause until
1339 released before reporting completion. This attribute may be
1340 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001341 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001342 be explicitly released.
1343
1344 """
James E. Blairfaf81982017-10-10 15:42:26 -07001345
1346 _job_class = RecordingAnsibleJob
1347
James E. Blairf5dbd002015-12-23 15:26:17 -08001348 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001349 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001350 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001351 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001352 self.hold_jobs_in_build = False
1353 self.lock = threading.Lock()
1354 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001355 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001357 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001358
James E. Blaira5dba232016-08-08 15:53:24 -07001359 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001360 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001361
1362 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001363 :arg Change change: The :py:class:`~tests.base.FakeChange`
1364 instance which should cause the job to fail. This job
1365 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001366
1367 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001368 l = self.fail_tests.get(name, [])
1369 l.append(change)
1370 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001371
James E. Blair962220f2016-08-03 11:22:38 -07001372 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001373 """Release a held build.
1374
1375 :arg str regex: A regular expression which, if supplied, will
1376 cause only builds with matching names to be released. If
1377 not supplied, all builds will be released.
1378
1379 """
James E. Blair962220f2016-08-03 11:22:38 -07001380 builds = self.running_builds[:]
1381 self.log.debug("Releasing build %s (%s)" % (regex,
1382 len(self.running_builds)))
1383 for build in builds:
1384 if not regex or re.match(regex, build.name):
1385 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001386 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001387 build.release()
1388 else:
1389 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001390 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001391 self.log.debug("Done releasing builds %s (%s)" %
1392 (regex, len(self.running_builds)))
1393
Paul Belanger174a8272017-03-14 13:20:10 -04001394 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001395 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001398 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001399 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001400 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001401 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001402 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001403
1404 def stopJob(self, job):
1405 self.log.debug("handle stop")
1406 parameters = json.loads(job.arguments)
1407 uuid = parameters['uuid']
1408 for build in self.running_builds:
1409 if build.unique == uuid:
1410 build.aborted = True
1411 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001412 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001413
James E. Blaira002b032017-04-18 10:35:48 -07001414 def stop(self):
1415 for build in self.running_builds:
1416 build.release()
1417 super(RecordingExecutorServer, self).stop()
1418
Joshua Hesketh50c21782016-10-13 21:34:14 +11001419
Clark Boylanb640e052014-04-03 16:41:46 -07001420class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001421 """A Gearman server for use in tests.
1422
1423 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1424 added to the queue but will not be distributed to workers
1425 until released. This attribute may be changed at any time and
1426 will take effect for subsequently enqueued jobs, but
1427 previously held jobs will still need to be explicitly
1428 released.
1429
1430 """
1431
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001432 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001433 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001434 self.hold_merge_jobs_in_queue = False
Fabien Boucher52252312018-01-18 19:54:34 +01001435 self.jobs_history = []
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001436 if use_ssl:
1437 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1438 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1439 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1440 else:
1441 ssl_ca = None
1442 ssl_cert = None
1443 ssl_key = None
1444
1445 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1446 ssl_cert=ssl_cert,
1447 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001448
1449 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001450 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1451 for job in job_queue:
Fabien Boucher52252312018-01-18 19:54:34 +01001452 self.jobs_history.append(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001453 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001454 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001455 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001456 elif job.name.startswith(b'merger:'):
1457 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001458 else:
1459 job.waiting = False
1460 if job.waiting:
1461 continue
1462 if job.name in connection.functions:
1463 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001464 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001465 connection.related_jobs[job.handle] = job
1466 job.worker_connection = connection
1467 job.running = True
1468 return job
1469 return None
1470
1471 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001472 """Release a held job.
1473
1474 :arg str regex: A regular expression which, if supplied, will
1475 cause only jobs with matching names to be released. If
1476 not supplied, all jobs will be released.
1477 """
Clark Boylanb640e052014-04-03 16:41:46 -07001478 released = False
1479 qlen = (len(self.high_queue) + len(self.normal_queue) +
1480 len(self.low_queue))
1481 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1482 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001483 match = False
1484 if job.name == b'executor:execute':
1485 parameters = json.loads(job.arguments.decode('utf8'))
1486 if not regex or re.match(regex, parameters.get('job')):
1487 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001488 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001489 if not regex:
1490 match = True
1491 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001492 self.log.debug("releasing queued job %s" %
1493 job.unique)
1494 job.waiting = False
1495 released = True
1496 else:
1497 self.log.debug("not releasing queued job %s" %
1498 job.unique)
1499 if released:
1500 self.wakeConnections()
1501 qlen = (len(self.high_queue) + len(self.normal_queue) +
1502 len(self.low_queue))
1503 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1504
1505
1506class FakeSMTP(object):
1507 log = logging.getLogger('zuul.FakeSMTP')
1508
1509 def __init__(self, messages, server, port):
1510 self.server = server
1511 self.port = port
1512 self.messages = messages
1513
1514 def sendmail(self, from_email, to_email, msg):
1515 self.log.info("Sending email from %s, to %s, with msg %s" % (
1516 from_email, to_email, msg))
1517
1518 headers = msg.split('\n\n', 1)[0]
1519 body = msg.split('\n\n', 1)[1]
1520
1521 self.messages.append(dict(
1522 from_email=from_email,
1523 to_email=to_email,
1524 msg=msg,
1525 headers=headers,
1526 body=body,
1527 ))
1528
1529 return True
1530
1531 def quit(self):
1532 return True
1533
1534
James E. Blairdce6cea2016-12-20 16:45:32 -08001535class FakeNodepool(object):
1536 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001537 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001538
1539 log = logging.getLogger("zuul.test.FakeNodepool")
1540
1541 def __init__(self, host, port, chroot):
1542 self.client = kazoo.client.KazooClient(
1543 hosts='%s:%s%s' % (host, port, chroot))
1544 self.client.start()
1545 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001546 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001547 self.thread = threading.Thread(target=self.run)
1548 self.thread.daemon = True
1549 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001550 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001551
1552 def stop(self):
1553 self._running = False
1554 self.thread.join()
1555 self.client.stop()
1556 self.client.close()
1557
1558 def run(self):
1559 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001560 try:
1561 self._run()
1562 except Exception:
1563 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001564 time.sleep(0.1)
1565
1566 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001567 if self.paused:
1568 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001569 for req in self.getNodeRequests():
1570 self.fulfillRequest(req)
1571
1572 def getNodeRequests(self):
1573 try:
1574 reqids = self.client.get_children(self.REQUEST_ROOT)
1575 except kazoo.exceptions.NoNodeError:
1576 return []
1577 reqs = []
1578 for oid in sorted(reqids):
1579 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001580 try:
1581 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001582 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001583 data['_oid'] = oid
1584 reqs.append(data)
1585 except kazoo.exceptions.NoNodeError:
1586 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001587 return reqs
1588
James E. Blaire18d4602017-01-05 11:17:28 -08001589 def getNodes(self):
1590 try:
1591 nodeids = self.client.get_children(self.NODE_ROOT)
1592 except kazoo.exceptions.NoNodeError:
1593 return []
1594 nodes = []
1595 for oid in sorted(nodeids):
1596 path = self.NODE_ROOT + '/' + oid
1597 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001598 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001599 data['_oid'] = oid
1600 try:
1601 lockfiles = self.client.get_children(path + '/lock')
1602 except kazoo.exceptions.NoNodeError:
1603 lockfiles = []
1604 if lockfiles:
1605 data['_lock'] = True
1606 else:
1607 data['_lock'] = False
1608 nodes.append(data)
1609 return nodes
1610
James E. Blaira38c28e2017-01-04 10:33:20 -08001611 def makeNode(self, request_id, node_type):
1612 now = time.time()
1613 path = '/nodepool/nodes/'
1614 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001615 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001616 provider='test-provider',
1617 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001618 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001619 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001620 public_ipv4='127.0.0.1',
1621 private_ipv4=None,
1622 public_ipv6=None,
1623 allocated_to=request_id,
1624 state='ready',
1625 state_time=now,
1626 created_time=now,
1627 updated_time=now,
1628 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001629 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001630 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001631 if 'fakeuser' in node_type:
1632 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001633 if 'windows' in node_type:
1634 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001635 if 'network' in node_type:
1636 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001637
Clint Byrumf322fe22017-05-10 20:53:12 -07001638 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001639 path = self.client.create(path, data,
1640 makepath=True,
1641 sequence=True)
1642 nodeid = path.split("/")[-1]
1643 return nodeid
1644
Krzysztof Klimonda37d54032017-10-25 12:16:47 +02001645 def removeNode(self, node):
1646 path = self.NODE_ROOT + '/' + node["_oid"]
1647 self.client.delete(path, recursive=True)
1648
James E. Blair6ab79e02017-01-06 10:10:17 -08001649 def addFailRequest(self, request):
1650 self.fail_requests.add(request['_oid'])
1651
James E. Blairdce6cea2016-12-20 16:45:32 -08001652 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001653 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001654 return
1655 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001656 oid = request['_oid']
1657 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001658
James E. Blair6ab79e02017-01-06 10:10:17 -08001659 if oid in self.fail_requests:
1660 request['state'] = 'failed'
1661 else:
1662 request['state'] = 'fulfilled'
1663 nodes = []
1664 for node in request['node_types']:
1665 nodeid = self.makeNode(oid, node)
1666 nodes.append(nodeid)
1667 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001668
James E. Blaira38c28e2017-01-04 10:33:20 -08001669 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001670 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001671 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001672 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001673 try:
1674 self.client.set(path, data)
1675 except kazoo.exceptions.NoNodeError:
1676 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001677
1678
James E. Blair498059b2016-12-20 13:50:13 -08001679class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001680 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001681 super(ChrootedKazooFixture, self).__init__()
1682
1683 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1684 if ':' in zk_host:
1685 host, port = zk_host.split(':')
1686 else:
1687 host = zk_host
1688 port = None
1689
1690 self.zookeeper_host = host
1691
1692 if not port:
1693 self.zookeeper_port = 2181
1694 else:
1695 self.zookeeper_port = int(port)
1696
Clark Boylan621ec9a2017-04-07 17:41:33 -07001697 self.test_id = test_id
1698
James E. Blair498059b2016-12-20 13:50:13 -08001699 def _setUp(self):
1700 # Make sure the test chroot paths do not conflict
1701 random_bits = ''.join(random.choice(string.ascii_lowercase +
1702 string.ascii_uppercase)
1703 for x in range(8))
1704
Clark Boylan621ec9a2017-04-07 17:41:33 -07001705 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001706 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1707
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001708 self.addCleanup(self._cleanup)
1709
James E. Blair498059b2016-12-20 13:50:13 -08001710 # Ensure the chroot path exists and clean up any pre-existing znodes.
1711 _tmp_client = kazoo.client.KazooClient(
1712 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1713 _tmp_client.start()
1714
1715 if _tmp_client.exists(self.zookeeper_chroot):
1716 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1717
1718 _tmp_client.ensure_path(self.zookeeper_chroot)
1719 _tmp_client.stop()
1720 _tmp_client.close()
1721
James E. Blair498059b2016-12-20 13:50:13 -08001722 def _cleanup(self):
1723 '''Remove the chroot path.'''
1724 # Need a non-chroot'ed client to remove the chroot path
1725 _tmp_client = kazoo.client.KazooClient(
1726 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1727 _tmp_client.start()
1728 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1729 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001730 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001731
1732
Joshua Heskethd78b4482015-09-14 16:56:34 -06001733class MySQLSchemaFixture(fixtures.Fixture):
1734 def setUp(self):
1735 super(MySQLSchemaFixture, self).setUp()
1736
1737 random_bits = ''.join(random.choice(string.ascii_lowercase +
1738 string.ascii_uppercase)
1739 for x in range(8))
1740 self.name = '%s_%s' % (random_bits, os.getpid())
1741 self.passwd = uuid.uuid4().hex
1742 db = pymysql.connect(host="localhost",
1743 user="openstack_citest",
1744 passwd="openstack_citest",
1745 db="openstack_citest")
1746 cur = db.cursor()
1747 cur.execute("create database %s" % self.name)
1748 cur.execute(
1749 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1750 (self.name, self.name, self.passwd))
1751 cur.execute("flush privileges")
1752
1753 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1754 self.passwd,
1755 self.name)
1756 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1757 self.addCleanup(self.cleanup)
1758
1759 def cleanup(self):
1760 db = pymysql.connect(host="localhost",
1761 user="openstack_citest",
1762 passwd="openstack_citest",
1763 db="openstack_citest")
1764 cur = db.cursor()
1765 cur.execute("drop database %s" % self.name)
1766 cur.execute("drop user '%s'@'localhost'" % self.name)
1767 cur.execute("flush privileges")
1768
1769
Maru Newby3fe5f852015-01-13 04:22:14 +00001770class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001771 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001772 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001773
James E. Blair1c236df2017-02-01 14:07:24 -08001774 def attachLogs(self, *args):
1775 def reader():
1776 self._log_stream.seek(0)
1777 while True:
1778 x = self._log_stream.read(4096)
1779 if not x:
1780 break
1781 yield x.encode('utf8')
1782 content = testtools.content.content_from_reader(
1783 reader,
1784 testtools.content_type.UTF8_TEXT,
1785 False)
1786 self.addDetail('logging', content)
1787
Clark Boylanb640e052014-04-03 16:41:46 -07001788 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001789 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001790 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1791 try:
1792 test_timeout = int(test_timeout)
1793 except ValueError:
1794 # If timeout value is invalid do not set a timeout.
1795 test_timeout = 0
1796 if test_timeout > 0:
1797 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1798
1799 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1800 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1801 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1802 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1803 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1804 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1805 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1806 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1807 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1808 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001809 self._log_stream = StringIO()
1810 self.addOnException(self.attachLogs)
1811 else:
1812 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001813
James E. Blair1c236df2017-02-01 14:07:24 -08001814 handler = logging.StreamHandler(self._log_stream)
1815 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1816 '%(levelname)-8s %(message)s')
1817 handler.setFormatter(formatter)
1818
1819 logger = logging.getLogger()
1820 logger.setLevel(logging.DEBUG)
1821 logger.addHandler(handler)
1822
Clark Boylan3410d532017-04-25 12:35:29 -07001823 # Make sure we don't carry old handlers around in process state
1824 # which slows down test runs
1825 self.addCleanup(logger.removeHandler, handler)
1826 self.addCleanup(handler.close)
1827 self.addCleanup(handler.flush)
1828
James E. Blair1c236df2017-02-01 14:07:24 -08001829 # NOTE(notmorgan): Extract logging overrides for specific
1830 # libraries from the OS_LOG_DEFAULTS env and create loggers
1831 # for each. This is used to limit the output during test runs
1832 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001833 log_defaults_from_env = os.environ.get(
1834 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001835 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001836
James E. Blairdce6cea2016-12-20 16:45:32 -08001837 if log_defaults_from_env:
1838 for default in log_defaults_from_env.split(','):
1839 try:
1840 name, level_str = default.split('=', 1)
1841 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001842 logger = logging.getLogger(name)
1843 logger.setLevel(level)
1844 logger.addHandler(handler)
1845 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001846 except ValueError:
1847 # NOTE(notmorgan): Invalid format of the log default,
1848 # skip and don't try and apply a logger for the
1849 # specified module
1850 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001851
Maru Newby3fe5f852015-01-13 04:22:14 +00001852
1853class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001854 """A test case with a functioning Zuul.
1855
1856 The following class variables are used during test setup and can
1857 be overidden by subclasses but are effectively read-only once a
1858 test method starts running:
1859
1860 :cvar str config_file: This points to the main zuul config file
1861 within the fixtures directory. Subclasses may override this
1862 to obtain a different behavior.
1863
1864 :cvar str tenant_config_file: This is the tenant config file
1865 (which specifies from what git repos the configuration should
1866 be loaded). It defaults to the value specified in
1867 `config_file` but can be overidden by subclasses to obtain a
1868 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001869 configuration. See also the :py:func:`simple_layout`
1870 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001871
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001872 :cvar bool create_project_keys: Indicates whether Zuul should
1873 auto-generate keys for each project, or whether the test
1874 infrastructure should insert dummy keys to save time during
1875 startup. Defaults to False.
1876
James E. Blaire7b99a02016-08-05 14:27:34 -07001877 The following are instance variables that are useful within test
1878 methods:
1879
1880 :ivar FakeGerritConnection fake_<connection>:
1881 A :py:class:`~tests.base.FakeGerritConnection` will be
1882 instantiated for each connection present in the config file
1883 and stored here. For instance, `fake_gerrit` will hold the
1884 FakeGerritConnection object for a connection named `gerrit`.
1885
1886 :ivar FakeGearmanServer gearman_server: An instance of
1887 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1888 server that all of the Zuul components in this test use to
1889 communicate with each other.
1890
Paul Belanger174a8272017-03-14 13:20:10 -04001891 :ivar RecordingExecutorServer executor_server: An instance of
1892 :py:class:`~tests.base.RecordingExecutorServer` which is the
1893 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001894
1895 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1896 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001897 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001898 list upon completion.
1899
1900 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1901 objects representing completed builds. They are appended to
1902 the list in the order they complete.
1903
1904 """
1905
James E. Blair83005782015-12-11 14:46:03 -08001906 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001907 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001908 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001909 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001910
1911 def _startMerger(self):
1912 self.merge_server = zuul.merger.server.MergeServer(self.config,
1913 self.connections)
1914 self.merge_server.start()
1915
Maru Newby3fe5f852015-01-13 04:22:14 +00001916 def setUp(self):
1917 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001918
1919 self.setupZK()
1920
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001921 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001922 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001923 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1924 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001925 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001926 tmp_root = tempfile.mkdtemp(
1927 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001928 self.test_root = os.path.join(tmp_root, "zuul-test")
1929 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001930 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001931 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001932 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001933 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1934 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001935
1936 if os.path.exists(self.test_root):
1937 shutil.rmtree(self.test_root)
1938 os.makedirs(self.test_root)
1939 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001940 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001941 os.makedirs(self.merger_state_root)
1942 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001943
1944 # Make per test copy of Configuration.
1945 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001946 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1947 if not os.path.exists(self.private_key_file):
1948 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1949 shutil.copy(src_private_key_file, self.private_key_file)
1950 shutil.copy('{}.pub'.format(src_private_key_file),
1951 '{}.pub'.format(self.private_key_file))
1952 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001953 self.config.set('scheduler', 'tenant_config',
1954 os.path.join(
1955 FIXTURE_DIR,
1956 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001957 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001958 self.config.set(
1959 'scheduler', 'command_socket',
1960 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001961 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001962 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001963 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001964 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001965 self.config.set(
1966 'executor', 'command_socket',
1967 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001968 self.config.set(
1969 'merger', 'command_socket',
1970 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001971
Clark Boylanb640e052014-04-03 16:41:46 -07001972 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001973 if self.config.has_section('statsd'):
1974 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001975 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001976
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001977 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001978
1979 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001980 self.log.info("Gearman server on port %s" %
1981 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001982 if self.use_ssl:
1983 self.log.info('SSL enabled for gearman')
1984 self.config.set(
1985 'gearman', 'ssl_ca',
1986 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1987 self.config.set(
1988 'gearman', 'ssl_cert',
1989 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1990 self.config.set(
1991 'gearman', 'ssl_key',
1992 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001993
Jesse Keating80730e62017-09-14 15:35:11 -06001994 self.rpcclient = zuul.rpcclient.RPCClient(
1995 self.config.get('gearman', 'server'),
1996 self.gearman_server.port,
1997 get_default(self.config, 'gearman', 'ssl_key'),
1998 get_default(self.config, 'gearman', 'ssl_cert'),
1999 get_default(self.config, 'gearman', 'ssl_ca'))
2000
James E. Blaire511d2f2016-12-08 15:22:26 -08002001 gerritsource.GerritSource.replication_timeout = 1.5
2002 gerritsource.GerritSource.replication_retry_interval = 0.5
2003 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002004
Joshua Hesketh352264b2015-08-11 23:42:08 +10002005 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002006 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002007
Jan Hruban6b71aff2015-10-22 16:58:08 +02002008 self.event_queues = [
2009 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002010 self.sched.trigger_event_queue,
2011 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002012 ]
2013
James E. Blairfef78942016-03-11 16:28:56 -08002014 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002015 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002016
Paul Belanger174a8272017-03-14 13:20:10 -04002017 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002018 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002019 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002020 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002021 _test_root=self.test_root,
2022 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002023 self.executor_server.start()
2024 self.history = self.executor_server.build_history
2025 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002026
Paul Belanger174a8272017-03-14 13:20:10 -04002027 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002028 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002029 self.merge_client = zuul.merger.client.MergeClient(
2030 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002031 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002032 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002033 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002034 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002035
James E. Blair0d5a36e2017-02-21 10:53:44 -05002036 self.fake_nodepool = FakeNodepool(
2037 self.zk_chroot_fixture.zookeeper_host,
2038 self.zk_chroot_fixture.zookeeper_port,
2039 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002040
Paul Belanger174a8272017-03-14 13:20:10 -04002041 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002042 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002043 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002044 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002045
Clark Boylanb640e052014-04-03 16:41:46 -07002046 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002047 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002048 # Cleanups are run in reverse order
2049 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002050 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002051 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002052
James E. Blairb9c0d772017-03-03 14:34:49 -08002053 self.sched.reconfigure(self.config)
2054 self.sched.resume()
2055
Tobias Henkel7df274b2017-05-26 17:41:11 +02002056 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002057 # Set up gerrit related fakes
2058 # Set a changes database so multiple FakeGerrit's can report back to
2059 # a virtual canonical database given by the configured hostname
2060 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002061 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002062
2063 def getGerritConnection(driver, name, config):
2064 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2065 con = FakeGerritConnection(driver, name, config,
2066 changes_db=db,
2067 upstream_root=self.upstream_root)
2068 self.event_queues.append(con.event_queue)
2069 setattr(self, 'fake_' + name, con)
2070 return con
2071
2072 self.useFixture(fixtures.MonkeyPatch(
2073 'zuul.driver.gerrit.GerritDriver.getConnection',
2074 getGerritConnection))
2075
Gregory Haynes4fc12542015-04-22 20:38:06 -07002076 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002077 server = config.get('server', 'github.com')
2078 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002079 con = FakeGithubConnection(driver, name, config,
Jesse Keating80730e62017-09-14 15:35:11 -06002080 self.rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -08002081 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002082 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002083 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002084 setattr(self, 'fake_' + name, con)
2085 return con
2086
2087 self.useFixture(fixtures.MonkeyPatch(
2088 'zuul.driver.github.GithubDriver.getConnection',
2089 getGithubConnection))
2090
James E. Blaire511d2f2016-12-08 15:22:26 -08002091 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002092 # TODO(jhesketh): This should come from lib.connections for better
2093 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002094 # Register connections from the config
2095 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002096
Joshua Hesketh352264b2015-08-11 23:42:08 +10002097 def FakeSMTPFactory(*args, **kw):
2098 args = [self.smtp_messages] + list(args)
2099 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002100
Joshua Hesketh352264b2015-08-11 23:42:08 +10002101 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002102
James E. Blaire511d2f2016-12-08 15:22:26 -08002103 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002104 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002105 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002106
James E. Blair83005782015-12-11 14:46:03 -08002107 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002108 # This creates the per-test configuration object. It can be
2109 # overriden by subclasses, but should not need to be since it
2110 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002111 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002112 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002113
James E. Blair39840362017-06-23 20:34:02 +01002114 sections = ['zuul', 'scheduler', 'executor', 'merger']
2115 for section in sections:
2116 if not self.config.has_section(section):
2117 self.config.add_section(section)
2118
James E. Blair06cc3922017-04-19 10:08:10 -07002119 if not self.setupSimpleLayout():
2120 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002121 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002122 self.tenant_config_file)
2123 git_path = os.path.join(
2124 os.path.dirname(
2125 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2126 'git')
2127 if os.path.exists(git_path):
2128 for reponame in os.listdir(git_path):
2129 project = reponame.replace('_', '/')
2130 self.copyDirToRepo(project,
2131 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002132 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002133 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002134 self.setupAllProjectKeys()
2135
James E. Blair06cc3922017-04-19 10:08:10 -07002136 def setupSimpleLayout(self):
2137 # If the test method has been decorated with a simple_layout,
2138 # use that instead of the class tenant_config_file. Set up a
2139 # single config-project with the specified layout, and
2140 # initialize repos for all of the 'project' entries which
2141 # appear in the layout.
2142 test_name = self.id().split('.')[-1]
2143 test = getattr(self, test_name)
2144 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002145 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002146 else:
2147 return False
2148
James E. Blairb70e55a2017-04-19 12:57:02 -07002149 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002150 path = os.path.join(FIXTURE_DIR, path)
2151 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002152 data = f.read()
2153 layout = yaml.safe_load(data)
2154 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002155 untrusted_projects = []
2156 for item in layout:
2157 if 'project' in item:
2158 name = item['project']['name']
2159 untrusted_projects.append(name)
2160 self.init_repo(name)
2161 self.addCommitToRepo(name, 'initial commit',
2162 files={'README': ''},
2163 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002164 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002165 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002166 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002167 for fn in zuul.configloader.as_list(
2168 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002169 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002170 for fn in zuul.configloader.as_list(
2171 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002172 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002173
2174 root = os.path.join(self.test_root, "config")
2175 if not os.path.exists(root):
2176 os.makedirs(root)
2177 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2178 config = [{'tenant':
2179 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002180 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002181 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002182 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002183 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002184 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002185 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002186 os.path.join(FIXTURE_DIR, f.name))
2187
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002188 self.init_repo('org/common-config')
2189 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002190 files, branch='master', tag='init')
2191
2192 return True
2193
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002194 def setupAllProjectKeys(self):
2195 if self.create_project_keys:
2196 return
2197
James E. Blair39840362017-06-23 20:34:02 +01002198 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002199 with open(os.path.join(FIXTURE_DIR, path)) as f:
2200 tenant_config = yaml.safe_load(f.read())
2201 for tenant in tenant_config:
2202 sources = tenant['tenant']['source']
2203 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002204 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002205 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002206 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002207 self.setupProjectKeys(source, project)
2208
2209 def setupProjectKeys(self, source, project):
2210 # Make sure we set up an RSA key for the project so that we
2211 # don't spend time generating one:
2212
James E. Blair6459db12017-06-29 14:57:20 -07002213 if isinstance(project, dict):
2214 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002215 key_root = os.path.join(self.state_root, 'keys')
2216 if not os.path.isdir(key_root):
2217 os.mkdir(key_root, 0o700)
2218 private_key_file = os.path.join(key_root, source, project + '.pem')
2219 private_key_dir = os.path.dirname(private_key_file)
2220 self.log.debug("Installing test keys for project %s at %s" % (
2221 project, private_key_file))
2222 if not os.path.isdir(private_key_dir):
2223 os.makedirs(private_key_dir)
2224 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2225 with open(private_key_file, 'w') as o:
2226 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002227
James E. Blair498059b2016-12-20 13:50:13 -08002228 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002229 self.zk_chroot_fixture = self.useFixture(
2230 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002231 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002232 self.zk_chroot_fixture.zookeeper_host,
2233 self.zk_chroot_fixture.zookeeper_port,
2234 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002235
James E. Blair96c6bf82016-01-15 16:20:40 -08002236 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002237 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002238
2239 files = {}
2240 for (dirpath, dirnames, filenames) in os.walk(source_path):
2241 for filename in filenames:
2242 test_tree_filepath = os.path.join(dirpath, filename)
2243 common_path = os.path.commonprefix([test_tree_filepath,
2244 source_path])
2245 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2246 with open(test_tree_filepath, 'r') as f:
2247 content = f.read()
2248 files[relative_filepath] = content
2249 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002250 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002251
James E. Blaire18d4602017-01-05 11:17:28 -08002252 def assertNodepoolState(self):
2253 # Make sure that there are no pending requests
2254
2255 requests = self.fake_nodepool.getNodeRequests()
2256 self.assertEqual(len(requests), 0)
2257
2258 nodes = self.fake_nodepool.getNodes()
2259 for node in nodes:
2260 self.assertFalse(node['_lock'], "Node %s is locked" %
2261 (node['_oid'],))
2262
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002263 def assertNoGeneratedKeys(self):
2264 # Make sure that Zuul did not generate any project keys
2265 # (unless it was supposed to).
2266
2267 if self.create_project_keys:
2268 return
2269
2270 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2271 test_key = i.read()
2272
2273 key_root = os.path.join(self.state_root, 'keys')
2274 for root, dirname, files in os.walk(key_root):
2275 for fn in files:
2276 with open(os.path.join(root, fn)) as f:
2277 self.assertEqual(test_key, f.read())
2278
Clark Boylanb640e052014-04-03 16:41:46 -07002279 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002280 self.log.debug("Assert final state")
2281 # Make sure no jobs are running
2282 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002283 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002284 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002285 gc.collect()
2286 for obj in gc.get_objects():
2287 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002288 self.log.debug("Leaked git repo object: 0x%x %s" %
2289 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002290 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002291 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002292 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002293 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002294 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002295 for tenant in self.sched.abide.tenants.values():
2296 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002297 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002298 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002299
2300 def shutdown(self):
2301 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002302 self.executor_server.hold_jobs_in_build = False
2303 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002304 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002305 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002306 if self.merge_server:
2307 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002308 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002309 self.sched.stop()
2310 self.sched.join()
2311 self.statsd.stop()
2312 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002313 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002314 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002315 self.fake_nodepool.stop()
2316 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002317 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002318 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002319 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002320 # Further the pydevd threads also need to be whitelisted so debugging
2321 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002322 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002323 'pydevd.CommandThread',
2324 'pydevd.Reader',
2325 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002326 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002327 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002328 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002329 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002330 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002331 log_str = ""
2332 for thread_id, stack_frame in sys._current_frames().items():
2333 log_str += "Thread: %s\n" % thread_id
2334 log_str += "".join(traceback.format_stack(stack_frame))
2335 self.log.debug(log_str)
2336 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002337
James E. Blaira002b032017-04-18 10:35:48 -07002338 def assertCleanShutdown(self):
2339 pass
2340
James E. Blairc4ba97a2017-04-19 16:26:24 -07002341 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002342 parts = project.split('/')
2343 path = os.path.join(self.upstream_root, *parts[:-1])
2344 if not os.path.exists(path):
2345 os.makedirs(path)
2346 path = os.path.join(self.upstream_root, project)
2347 repo = git.Repo.init(path)
2348
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002349 with repo.config_writer() as config_writer:
2350 config_writer.set_value('user', 'email', 'user@example.com')
2351 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002352
Clark Boylanb640e052014-04-03 16:41:46 -07002353 repo.index.commit('initial commit')
2354 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002355 if tag:
2356 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002357
James E. Blair97d902e2014-08-21 13:25:56 -07002358 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002359 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002360 repo.git.clean('-x', '-f', '-d')
2361
James E. Blair97d902e2014-08-21 13:25:56 -07002362 def create_branch(self, project, branch):
2363 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002364 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002365 fn = os.path.join(path, 'README')
2366
2367 branch_head = repo.create_head(branch)
2368 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002369 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002370 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002371 f.close()
2372 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002373 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002374
James E. Blair97d902e2014-08-21 13:25:56 -07002375 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002376 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002377 repo.git.clean('-x', '-f', '-d')
2378
James E. Blairda5bb7e2018-01-22 16:12:17 -08002379 def delete_branch(self, project, branch):
2380 path = os.path.join(self.upstream_root, project)
2381 repo = git.Repo(path)
2382 repo.head.reference = repo.heads['master']
2383 zuul.merger.merger.reset_repo_to_head(repo)
2384 repo.delete_head(repo.heads[branch], force=True)
2385
Sachi King9f16d522016-03-16 12:20:45 +11002386 def create_commit(self, project):
2387 path = os.path.join(self.upstream_root, project)
2388 repo = git.Repo(path)
2389 repo.head.reference = repo.heads['master']
2390 file_name = os.path.join(path, 'README')
2391 with open(file_name, 'a') as f:
2392 f.write('creating fake commit\n')
2393 repo.index.add([file_name])
2394 commit = repo.index.commit('Creating a fake commit')
2395 return commit.hexsha
2396
James E. Blairf4a5f022017-04-18 14:01:10 -07002397 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002398 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002399 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002400 while len(self.builds):
2401 self.release(self.builds[0])
2402 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002403 i += 1
2404 if count is not None and i >= count:
2405 break
James E. Blairb8c16472015-05-05 14:55:26 -07002406
James E. Blairdf25ddc2017-07-08 07:57:09 -07002407 def getSortedBuilds(self):
2408 "Return the list of currently running builds sorted by name"
2409
2410 return sorted(self.builds, key=lambda x: x.name)
2411
Clark Boylanb640e052014-04-03 16:41:46 -07002412 def release(self, job):
2413 if isinstance(job, FakeBuild):
2414 job.release()
2415 else:
2416 job.waiting = False
2417 self.log.debug("Queued job %s released" % job.unique)
2418 self.gearman_server.wakeConnections()
2419
2420 def getParameter(self, job, name):
2421 if isinstance(job, FakeBuild):
2422 return job.parameters[name]
2423 else:
2424 parameters = json.loads(job.arguments)
2425 return parameters[name]
2426
Clark Boylanb640e052014-04-03 16:41:46 -07002427 def haveAllBuildsReported(self):
2428 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002429 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002430 return False
2431 # Find out if every build that the worker has completed has been
2432 # reported back to Zuul. If it hasn't then that means a Gearman
2433 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002434 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002435 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002436 if not zbuild:
2437 # It has already been reported
2438 continue
2439 # It hasn't been reported yet.
2440 return False
2441 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002442 worker = self.executor_server.executor_worker
2443 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002444 if connection.state == 'GRAB_WAIT':
2445 return False
2446 return True
2447
2448 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002449 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002450 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002451 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002452 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002453 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002454 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002455 for j in conn.related_jobs.values():
2456 if j.unique == build.uuid:
2457 client_job = j
2458 break
2459 if not client_job:
2460 self.log.debug("%s is not known to the gearman client" %
2461 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002462 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002463 if not client_job.handle:
2464 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002465 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002466 server_job = self.gearman_server.jobs.get(client_job.handle)
2467 if not server_job:
2468 self.log.debug("%s is not known to the gearman server" %
2469 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002470 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002471 if not hasattr(server_job, 'waiting'):
2472 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002473 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002474 if server_job.waiting:
2475 continue
James E. Blair17302972016-08-10 16:11:42 -07002476 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002477 self.log.debug("%s has not reported start" % build)
2478 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002479 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002480 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002481 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002482 if worker_build:
2483 if worker_build.isWaiting():
2484 continue
2485 else:
2486 self.log.debug("%s is running" % worker_build)
2487 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002488 else:
James E. Blair962220f2016-08-03 11:22:38 -07002489 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002490 return False
James E. Blaira002b032017-04-18 10:35:48 -07002491 for (build_uuid, job_worker) in \
2492 self.executor_server.job_workers.items():
2493 if build_uuid not in seen_builds:
2494 self.log.debug("%s is not finalized" % build_uuid)
2495 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002496 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002497
James E. Blairdce6cea2016-12-20 16:45:32 -08002498 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002499 if self.fake_nodepool.paused:
2500 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002501 if self.sched.nodepool.requests:
2502 return False
2503 return True
2504
James E. Blaira615c362017-10-02 17:34:42 -07002505 def areAllMergeJobsWaiting(self):
2506 for client_job in list(self.merge_client.jobs):
2507 if not client_job.handle:
2508 self.log.debug("%s has no handle" % client_job)
2509 return False
2510 server_job = self.gearman_server.jobs.get(client_job.handle)
2511 if not server_job:
2512 self.log.debug("%s is not known to the gearman server" %
2513 client_job)
2514 return False
2515 if not hasattr(server_job, 'waiting'):
2516 self.log.debug("%s is being enqueued" % server_job)
2517 return False
2518 if server_job.waiting:
2519 self.log.debug("%s is waiting" % server_job)
2520 continue
2521 self.log.debug("%s is not waiting" % server_job)
2522 return False
2523 return True
2524
Jan Hruban6b71aff2015-10-22 16:58:08 +02002525 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002526 for event_queue in self.event_queues:
2527 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002528
2529 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002530 for event_queue in self.event_queues:
2531 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002532
Clark Boylanb640e052014-04-03 16:41:46 -07002533 def waitUntilSettled(self):
2534 self.log.debug("Waiting until settled...")
2535 start = time.time()
2536 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002537 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002538 self.log.error("Timeout waiting for Zuul to settle")
2539 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002540 for event_queue in self.event_queues:
2541 self.log.error(" %s: %s" %
2542 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002543 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002544 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002545 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002546 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002547 self.log.error("All requests completed: %s" %
2548 (self.areAllNodeRequestsComplete(),))
2549 self.log.error("Merge client jobs: %s" %
2550 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002551 raise Exception("Timeout waiting for Zuul to settle")
2552 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002553
Paul Belanger174a8272017-03-14 13:20:10 -04002554 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002555 # have all build states propogated to zuul?
2556 if self.haveAllBuildsReported():
2557 # Join ensures that the queue is empty _and_ events have been
2558 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002559 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002560 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002561 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002562 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002563 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002564 self.areAllNodeRequestsComplete() and
2565 all(self.eventQueuesEmpty())):
2566 # The queue empty check is placed at the end to
2567 # ensure that if a component adds an event between
2568 # when locked the run handler and checked that the
2569 # components were stable, we don't erroneously
2570 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002571 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002572 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002573 self.log.debug("...settled.")
2574 return
2575 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002576 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002577 self.sched.wake_event.wait(0.1)
2578
2579 def countJobResults(self, jobs, result):
2580 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002581 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002582
Monty Taylor0d926122017-05-24 08:07:56 -05002583 def getBuildByName(self, name):
2584 for build in self.builds:
2585 if build.name == name:
2586 return build
2587 raise Exception("Unable to find build %s" % name)
2588
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002589 def assertJobNotInHistory(self, name, project=None):
2590 for job in self.history:
2591 if (project is None or
2592 job.parameters['zuul']['project']['name'] == project):
2593 self.assertNotEqual(job.name, name,
2594 'Job %s found in history' % name)
2595
James E. Blair96c6bf82016-01-15 16:20:40 -08002596 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002597 for job in self.history:
2598 if (job.name == name and
2599 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002600 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002601 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002602 raise Exception("Unable to find job %s in history" % name)
2603
2604 def assertEmptyQueues(self):
2605 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002606 for tenant in self.sched.abide.tenants.values():
2607 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002608 for pipeline_queue in pipeline.queues:
2609 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002610 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002611 pipeline.name, pipeline_queue.name,
2612 pipeline_queue.queue))
2613 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002614 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002615
2616 def assertReportedStat(self, key, value=None, kind=None):
2617 start = time.time()
2618 while time.time() < (start + 5):
2619 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002620 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002621 if key == k:
2622 if value is None and kind is None:
2623 return
2624 elif value:
2625 if value == v:
2626 return
2627 elif kind:
2628 if v.endswith('|' + kind):
2629 return
2630 time.sleep(0.1)
2631
Clark Boylanb640e052014-04-03 16:41:46 -07002632 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002633
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002634 def assertBuilds(self, builds):
2635 """Assert that the running builds are as described.
2636
2637 The list of running builds is examined and must match exactly
2638 the list of builds described by the input.
2639
2640 :arg list builds: A list of dictionaries. Each item in the
2641 list must match the corresponding build in the build
2642 history, and each element of the dictionary must match the
2643 corresponding attribute of the build.
2644
2645 """
James E. Blair3158e282016-08-19 09:34:11 -07002646 try:
2647 self.assertEqual(len(self.builds), len(builds))
2648 for i, d in enumerate(builds):
2649 for k, v in d.items():
2650 self.assertEqual(
2651 getattr(self.builds[i], k), v,
2652 "Element %i in builds does not match" % (i,))
2653 except Exception:
2654 for build in self.builds:
2655 self.log.error("Running build: %s" % build)
2656 else:
2657 self.log.error("No running builds")
2658 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002659
James E. Blairb536ecc2016-08-31 10:11:42 -07002660 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002661 """Assert that the completed builds are as described.
2662
2663 The list of completed builds is examined and must match
2664 exactly the list of builds described by the input.
2665
2666 :arg list history: A list of dictionaries. Each item in the
2667 list must match the corresponding build in the build
2668 history, and each element of the dictionary must match the
2669 corresponding attribute of the build.
2670
James E. Blairb536ecc2016-08-31 10:11:42 -07002671 :arg bool ordered: If true, the history must match the order
2672 supplied, if false, the builds are permitted to have
2673 arrived in any order.
2674
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002675 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002676 def matches(history_item, item):
2677 for k, v in item.items():
2678 if getattr(history_item, k) != v:
2679 return False
2680 return True
James E. Blair3158e282016-08-19 09:34:11 -07002681 try:
2682 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002683 if ordered:
2684 for i, d in enumerate(history):
2685 if not matches(self.history[i], d):
2686 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002687 "Element %i in history does not match %s" %
2688 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002689 else:
2690 unseen = self.history[:]
2691 for i, d in enumerate(history):
2692 found = False
2693 for unseen_item in unseen:
2694 if matches(unseen_item, d):
2695 found = True
2696 unseen.remove(unseen_item)
2697 break
2698 if not found:
2699 raise Exception("No match found for element %i "
2700 "in history" % (i,))
2701 if unseen:
2702 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002703 except Exception:
2704 for build in self.history:
2705 self.log.error("Completed build: %s" % build)
2706 else:
2707 self.log.error("No completed builds")
2708 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002709
James E. Blair6ac368c2016-12-22 18:07:20 -08002710 def printHistory(self):
2711 """Log the build history.
2712
2713 This can be useful during tests to summarize what jobs have
2714 completed.
2715
2716 """
2717 self.log.debug("Build history:")
2718 for build in self.history:
2719 self.log.debug(build)
2720
James E. Blair59fdbac2015-12-07 17:08:06 -08002721 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002722 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2723
James E. Blair9ea70072017-04-19 16:05:30 -07002724 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002725 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002726 if not os.path.exists(root):
2727 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002728 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2729 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002730- tenant:
2731 name: openstack
2732 source:
2733 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002734 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002735 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002736 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002737 - org/project
2738 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002739 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002740 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002741 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002742 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002743 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002744
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002745 def addTagToRepo(self, project, name, sha):
2746 path = os.path.join(self.upstream_root, project)
2747 repo = git.Repo(path)
2748 repo.git.tag(name, sha)
2749
2750 def delTagFromRepo(self, project, name):
2751 path = os.path.join(self.upstream_root, project)
2752 repo = git.Repo(path)
2753 repo.git.tag('-d', name)
2754
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002755 def addCommitToRepo(self, project, message, files,
2756 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002757 path = os.path.join(self.upstream_root, project)
2758 repo = git.Repo(path)
2759 repo.head.reference = branch
2760 zuul.merger.merger.reset_repo_to_head(repo)
2761 for fn, content in files.items():
2762 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002763 try:
2764 os.makedirs(os.path.dirname(fn))
2765 except OSError:
2766 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002767 with open(fn, 'w') as f:
2768 f.write(content)
2769 repo.index.add([fn])
2770 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002771 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002772 repo.heads[branch].commit = commit
2773 repo.head.reference = branch
2774 repo.git.clean('-x', '-f', '-d')
2775 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002776 if tag:
2777 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002778 return before
2779
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002780 def commitConfigUpdate(self, project_name, source_name):
2781 """Commit an update to zuul.yaml
2782
2783 This overwrites the zuul.yaml in the specificed project with
2784 the contents specified.
2785
2786 :arg str project_name: The name of the project containing
2787 zuul.yaml (e.g., common-config)
2788
2789 :arg str source_name: The path to the file (underneath the
2790 test fixture directory) whose contents should be used to
2791 replace zuul.yaml.
2792 """
2793
2794 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002795 files = {}
2796 with open(source_path, 'r') as f:
2797 data = f.read()
2798 layout = yaml.safe_load(data)
2799 files['zuul.yaml'] = data
2800 for item in layout:
2801 if 'job' in item:
2802 jobname = item['job']['name']
2803 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002804 before = self.addCommitToRepo(
2805 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002806 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002807 return before
2808
Clint Byrum627ba362017-08-14 13:20:40 -07002809 def newTenantConfig(self, source_name):
2810 """ Use this to update the tenant config file in tests
2811
2812 This will update self.tenant_config_file to point to a temporary file
2813 for the duration of this particular test. The content of that file will
2814 be taken from FIXTURE_DIR/source_name
2815
2816 After the test the original value of self.tenant_config_file will be
2817 restored.
2818
2819 :arg str source_name: The path of the file under
2820 FIXTURE_DIR that will be used to populate the new tenant
2821 config file.
2822 """
2823 source_path = os.path.join(FIXTURE_DIR, source_name)
2824 orig_tenant_config_file = self.tenant_config_file
2825 with tempfile.NamedTemporaryFile(
2826 delete=False, mode='wb') as new_tenant_config:
2827 self.tenant_config_file = new_tenant_config.name
2828 with open(source_path, mode='rb') as source_tenant_config:
2829 new_tenant_config.write(source_tenant_config.read())
2830 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2831 self.setupAllProjectKeys()
2832 self.log.debug(
2833 'tenant_config_file = {}'.format(self.tenant_config_file))
2834
2835 def _restoreTenantConfig():
2836 self.log.debug(
2837 'restoring tenant_config_file = {}'.format(
2838 orig_tenant_config_file))
2839 os.unlink(self.tenant_config_file)
2840 self.tenant_config_file = orig_tenant_config_file
2841 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2842 self.addCleanup(_restoreTenantConfig)
2843
James E. Blair7fc8daa2016-08-08 15:37:15 -07002844 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002845
James E. Blair7fc8daa2016-08-08 15:37:15 -07002846 """Inject a Fake (Gerrit) event.
2847
2848 This method accepts a JSON-encoded event and simulates Zuul
2849 having received it from Gerrit. It could (and should)
2850 eventually apply to any connection type, but is currently only
2851 used with Gerrit connections. The name of the connection is
2852 used to look up the corresponding server, and the event is
2853 simulated as having been received by all Zuul connections
2854 attached to that server. So if two Gerrit connections in Zuul
2855 are connected to the same Gerrit server, and you invoke this
2856 method specifying the name of one of them, the event will be
2857 received by both.
2858
2859 .. note::
2860
2861 "self.fake_gerrit.addEvent" calls should be migrated to
2862 this method.
2863
2864 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002865 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002866 :arg str event: The JSON-encoded event.
2867
2868 """
2869 specified_conn = self.connections.connections[connection]
2870 for conn in self.connections.connections.values():
2871 if (isinstance(conn, specified_conn.__class__) and
2872 specified_conn.server == conn.server):
2873 conn.addEvent(event)
2874
James E. Blaird8af5422017-05-24 13:59:40 -07002875 def getUpstreamRepos(self, projects):
2876 """Return upstream git repo objects for the listed projects
2877
2878 :arg list projects: A list of strings, each the canonical name
2879 of a project.
2880
2881 :returns: A dictionary of {name: repo} for every listed
2882 project.
2883 :rtype: dict
2884
2885 """
2886
2887 repos = {}
2888 for project in projects:
2889 # FIXME(jeblair): the upstream root does not yet have a
2890 # hostname component; that needs to be added, and this
2891 # line removed:
2892 tmp_project_name = '/'.join(project.split('/')[1:])
2893 path = os.path.join(self.upstream_root, tmp_project_name)
2894 repo = git.Repo(path)
2895 repos[project] = repo
2896 return repos
2897
James E. Blair3f876d52016-07-22 13:07:14 -07002898
2899class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002900 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002901 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002902
Jamie Lennox7655b552017-03-17 12:33:38 +11002903 @contextmanager
2904 def jobLog(self, build):
2905 """Print job logs on assertion errors
2906
2907 This method is a context manager which, if it encounters an
2908 ecxeption, adds the build log to the debug output.
2909
2910 :arg Build build: The build that's being asserted.
2911 """
2912 try:
2913 yield
2914 except Exception:
2915 path = os.path.join(self.test_root, build.uuid,
2916 'work', 'logs', 'job-output.txt')
2917 with open(path) as f:
2918 self.log.debug(f.read())
2919 raise
2920
Joshua Heskethd78b4482015-09-14 16:56:34 -06002921
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002922class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002923 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002924 use_ssl = True
2925
2926
Joshua Heskethd78b4482015-09-14 16:56:34 -06002927class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002928 def setup_config(self):
2929 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002930 for section_name in self.config.sections():
2931 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2932 section_name, re.I)
2933 if not con_match:
2934 continue
2935
2936 if self.config.get(section_name, 'driver') == 'sql':
2937 f = MySQLSchemaFixture()
2938 self.useFixture(f)
2939 if (self.config.get(section_name, 'dburi') ==
2940 '$MYSQL_FIXTURE_DBURI$'):
2941 self.config.set(section_name, 'dburi', f.dburi)