blob: f68f59a58156a6675e800a8de164ff331a2d2dee [file] [log] [blame]
Clark Boylanb640e052014-04-03 16:41:46 -07001#!/usr/bin/env python
2
3# Copyright 2012 Hewlett-Packard Development Company, L.P.
James E. Blair498059b2016-12-20 13:50:13 -08004# Copyright 2016 Red Hat, Inc.
Clark Boylanb640e052014-04-03 16:41:46 -07005#
6# Licensed under the Apache License, Version 2.0 (the "License"); you may
7# not use this file except in compliance with the License. You may obtain
8# a copy of the License at
9#
10# http://www.apache.org/licenses/LICENSE-2.0
11#
12# Unless required by applicable law or agreed to in writing, software
13# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15# License for the specific language governing permissions and limitations
16# under the License.
17
Monty Taylorb934c1a2017-06-16 19:31:47 -050018import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110019from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080020import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070021import gc
22import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050023from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070024import json
25import logging
26import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050027import queue
Clark Boylanb640e052014-04-03 16:41:46 -070028import random
29import re
30import select
31import shutil
32import socket
33import string
34import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080035import sys
James E. Blairf84026c2015-12-08 16:11:46 -080036import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070037import threading
Clark Boylan8208c192017-04-24 18:08:08 -070038import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070039import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060040import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050041import urllib
Joshua Heskethd78b4482015-09-14 16:56:34 -060042
Clark Boylanb640e052014-04-03 16:41:46 -070043import git
44import gear
45import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080046import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080047import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060048import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070049import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080050import testtools.content
51import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080052from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000053import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070054
James E. Blair6bacffb2018-01-05 13:45:25 -080055import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080056import zuul.driver.gerrit.gerritsource as gerritsource
57import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070058import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070059import zuul.scheduler
Paul Belanger174a8272017-03-14 13:20:10 -040060import zuul.executor.server
61import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080062import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070063import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070064import zuul.merger.merger
65import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020066import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070067import zuul.nodepool
Jesse Keating80730e62017-09-14 15:35:11 -060068import zuul.rpcclient
James E. Blairdce6cea2016-12-20 16:45:32 -080069import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070070import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010071from zuul.exceptions import MergeFailure
Jesse Keating80730e62017-09-14 15:35:11 -060072from zuul.lib.config import get_default
Clark Boylanb640e052014-04-03 16:41:46 -070073
74FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
75 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080076
77KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070078
Clark Boylanb640e052014-04-03 16:41:46 -070079
80def repack_repo(path):
81 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
82 output = subprocess.Popen(cmd, close_fds=True,
83 stdout=subprocess.PIPE,
84 stderr=subprocess.PIPE)
85 out = output.communicate()
86 if output.returncode:
87 raise Exception("git repack returned %d" % output.returncode)
88 return out
89
90
91def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040092 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070093
94
James E. Blaira190f3b2015-01-05 14:56:54 -080095def iterate_timeout(max_seconds, purpose):
96 start = time.time()
97 count = 0
98 while (time.time() < start + max_seconds):
99 count += 1
100 yield count
101 time.sleep(0)
102 raise Exception("Timeout waiting for %s" % purpose)
103
104
Jesse Keating436a5452017-04-20 11:48:41 -0700105def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700106 """Specify a layout file for use by a test method.
107
108 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700109 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700110
111 Some tests require only a very simple configuration. For those,
112 establishing a complete config directory hierachy is too much
113 work. In those cases, you can add a simple zuul.yaml file to the
114 test fixtures directory (in fixtures/layouts/foo.yaml) and use
115 this decorator to indicate the test method should use that rather
116 than the tenant config file specified by the test class.
117
118 The decorator will cause that layout file to be added to a
119 config-project called "common-config" and each "project" instance
120 referenced in the layout file will have a git repo automatically
121 initialized.
122 """
123
124 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700125 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700126 return test
127 return decorator
128
129
Gregory Haynes4fc12542015-04-22 20:38:06 -0700130class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700131 _common_path_default = "refs/changes"
132 _points_to_commits_only = True
133
134
Gregory Haynes4fc12542015-04-22 20:38:06 -0700135class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200136 categories = {'Approved': ('Approved', -1, 1),
137 'Code-Review': ('Code-Review', -2, 2),
138 'Verified': ('Verified', -2, 2)}
139
Clark Boylanb640e052014-04-03 16:41:46 -0700140 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700141 status='NEW', upstream_root=None, files={},
142 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700144 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700145 self.reported = 0
146 self.queried = 0
147 self.patchsets = []
148 self.number = number
149 self.project = project
150 self.branch = branch
151 self.subject = subject
152 self.latest_patchset = 0
153 self.depends_on_change = None
154 self.needed_by_changes = []
155 self.fail_merge = False
156 self.messages = []
157 self.data = {
158 'branch': branch,
159 'comments': [],
160 'commitMessage': subject,
161 'createdOn': time.time(),
162 'id': 'I' + random_sha1(),
163 'lastUpdated': time.time(),
164 'number': str(number),
165 'open': status == 'NEW',
166 'owner': {'email': 'user@example.com',
167 'name': 'User Name',
168 'username': 'username'},
169 'patchSets': self.patchsets,
170 'project': project,
171 'status': status,
172 'subject': subject,
173 'submitRecords': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800174 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700175
176 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700177 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700178 self.data['submitRecords'] = self.getSubmitRecords()
179 self.open = status == 'NEW'
180
James E. Blair289f5932017-07-27 15:02:29 -0700181 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700182 path = os.path.join(self.upstream_root, self.project)
183 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700184 if parent is None:
185 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700186 ref = GerritChangeReference.create(
187 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700188 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700190 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700191 repo.git.clean('-x', '-f', '-d')
192
193 path = os.path.join(self.upstream_root, self.project)
194 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700195 for fn, content in files.items():
196 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700197 if content is None:
198 os.unlink(fn)
199 repo.index.remove([fn])
200 else:
201 d = os.path.dirname(fn)
202 if not os.path.exists(d):
203 os.makedirs(d)
204 with open(fn, 'w') as f:
205 f.write(content)
206 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700207 else:
208 for fni in range(100):
209 fn = os.path.join(path, str(fni))
210 f = open(fn, 'w')
211 for ci in range(4096):
212 f.write(random.choice(string.printable))
213 f.close()
214 repo.index.add([fn])
215
216 r = repo.index.commit(msg)
217 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700218 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700219 repo.git.clean('-x', '-f', '-d')
220 repo.heads['master'].checkout()
221 return r
222
James E. Blair289f5932017-07-27 15:02:29 -0700223 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700224 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700225 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700226 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700227 data = ("test %s %s %s\n" %
228 (self.branch, self.number, self.latest_patchset))
229 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700230 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700231 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700232 ps_files = [{'file': '/COMMIT_MSG',
233 'type': 'ADDED'},
234 {'file': 'README',
235 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700236 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700237 ps_files.append({'file': f, 'type': 'ADDED'})
238 d = {'approvals': [],
239 'createdOn': time.time(),
240 'files': ps_files,
241 'number': str(self.latest_patchset),
242 'ref': 'refs/changes/1/%s/%s' % (self.number,
243 self.latest_patchset),
244 'revision': c.hexsha,
245 'uploader': {'email': 'user@example.com',
246 'name': 'User name',
247 'username': 'user'}}
248 self.data['currentPatchSet'] = d
249 self.patchsets.append(d)
250 self.data['submitRecords'] = self.getSubmitRecords()
251
252 def getPatchsetCreatedEvent(self, patchset):
253 event = {"type": "patchset-created",
254 "change": {"project": self.project,
255 "branch": self.branch,
256 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
257 "number": str(self.number),
258 "subject": self.subject,
259 "owner": {"name": "User Name"},
260 "url": "https://hostname/3"},
261 "patchSet": self.patchsets[patchset - 1],
262 "uploader": {"name": "User Name"}}
263 return event
264
265 def getChangeRestoredEvent(self):
266 event = {"type": "change-restored",
267 "change": {"project": self.project,
268 "branch": self.branch,
269 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
270 "number": str(self.number),
271 "subject": self.subject,
272 "owner": {"name": "User Name"},
273 "url": "https://hostname/3"},
274 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100275 "patchSet": self.patchsets[-1],
276 "reason": ""}
277 return event
278
279 def getChangeAbandonedEvent(self):
280 event = {"type": "change-abandoned",
281 "change": {"project": self.project,
282 "branch": self.branch,
283 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
284 "number": str(self.number),
285 "subject": self.subject,
286 "owner": {"name": "User Name"},
287 "url": "https://hostname/3"},
288 "abandoner": {"name": "User Name"},
289 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700290 "reason": ""}
291 return event
292
293 def getChangeCommentEvent(self, patchset):
294 event = {"type": "comment-added",
295 "change": {"project": self.project,
296 "branch": self.branch,
297 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
298 "number": str(self.number),
299 "subject": self.subject,
300 "owner": {"name": "User Name"},
301 "url": "https://hostname/3"},
302 "patchSet": self.patchsets[patchset - 1],
303 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200304 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700305 "description": "Code-Review",
306 "value": "0"}],
307 "comment": "This is a comment"}
308 return event
309
James E. Blairc2a5ed72017-02-20 14:12:01 -0500310 def getChangeMergedEvent(self):
311 event = {"submitter": {"name": "Jenkins",
312 "username": "jenkins"},
313 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
314 "patchSet": self.patchsets[-1],
315 "change": self.data,
316 "type": "change-merged",
317 "eventCreatedOn": 1487613810}
318 return event
319
James E. Blair8cce42e2016-10-18 08:18:36 -0700320 def getRefUpdatedEvent(self):
321 path = os.path.join(self.upstream_root, self.project)
322 repo = git.Repo(path)
323 oldrev = repo.heads[self.branch].commit.hexsha
324
325 event = {
326 "type": "ref-updated",
327 "submitter": {
328 "name": "User Name",
329 },
330 "refUpdate": {
331 "oldRev": oldrev,
332 "newRev": self.patchsets[-1]['revision'],
333 "refName": self.branch,
334 "project": self.project,
335 }
336 }
337 return event
338
Joshua Hesketh642824b2014-07-01 17:54:59 +1000339 def addApproval(self, category, value, username='reviewer_john',
340 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700341 if not granted_on:
342 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000343 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200344 'description': self.categories[category][0],
345 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000346 'value': str(value),
347 'by': {
348 'username': username,
349 'email': username + '@example.com',
350 },
351 'grantedOn': int(granted_on)
352 }
Clark Boylanb640e052014-04-03 16:41:46 -0700353 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200354 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700355 del self.patchsets[-1]['approvals'][i]
356 self.patchsets[-1]['approvals'].append(approval)
357 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000358 'author': {'email': 'author@example.com',
359 'name': 'Patchset Author',
360 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700361 'change': {'branch': self.branch,
362 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
363 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000364 'owner': {'email': 'owner@example.com',
365 'name': 'Change Owner',
366 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700367 'project': self.project,
368 'subject': self.subject,
369 'topic': 'master',
370 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000371 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700372 'patchSet': self.patchsets[-1],
373 'type': 'comment-added'}
374 self.data['submitRecords'] = self.getSubmitRecords()
375 return json.loads(json.dumps(event))
376
377 def getSubmitRecords(self):
378 status = {}
379 for cat in self.categories.keys():
380 status[cat] = 0
381
382 for a in self.patchsets[-1]['approvals']:
383 cur = status[a['type']]
384 cat_min, cat_max = self.categories[a['type']][1:]
385 new = int(a['value'])
386 if new == cat_min:
387 cur = new
388 elif abs(new) > abs(cur):
389 cur = new
390 status[a['type']] = cur
391
392 labels = []
393 ok = True
394 for typ, cat in self.categories.items():
395 cur = status[typ]
396 cat_min, cat_max = cat[1:]
397 if cur == cat_min:
398 value = 'REJECT'
399 ok = False
400 elif cur == cat_max:
401 value = 'OK'
402 else:
403 value = 'NEED'
404 ok = False
405 labels.append({'label': cat[0], 'status': value})
406 if ok:
407 return [{'status': 'OK'}]
408 return [{'status': 'NOT_READY',
409 'labels': labels}]
410
411 def setDependsOn(self, other, patchset):
412 self.depends_on_change = other
413 d = {'id': other.data['id'],
414 'number': other.data['number'],
415 'ref': other.patchsets[patchset - 1]['ref']
416 }
417 self.data['dependsOn'] = [d]
418
419 other.needed_by_changes.append(self)
420 needed = other.data.get('neededBy', [])
421 d = {'id': self.data['id'],
422 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700423 'ref': self.patchsets[-1]['ref'],
424 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700425 }
426 needed.append(d)
427 other.data['neededBy'] = needed
428
429 def query(self):
430 self.queried += 1
431 d = self.data.get('dependsOn')
432 if d:
433 d = d[0]
434 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
435 d['isCurrentPatchSet'] = True
436 else:
437 d['isCurrentPatchSet'] = False
438 return json.loads(json.dumps(self.data))
439
440 def setMerged(self):
441 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000442 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700443 return
444 if self.fail_merge:
445 return
446 self.data['status'] = 'MERGED'
447 self.open = False
448
449 path = os.path.join(self.upstream_root, self.project)
450 repo = git.Repo(path)
451 repo.heads[self.branch].commit = \
452 repo.commit(self.patchsets[-1]['revision'])
453
454 def setReported(self):
455 self.reported += 1
456
457
James E. Blaire511d2f2016-12-08 15:22:26 -0800458class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700459 """A Fake Gerrit connection for use in tests.
460
461 This subclasses
462 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
463 ability for tests to add changes to the fake Gerrit it represents.
464 """
465
Joshua Hesketh352264b2015-08-11 23:42:08 +1000466 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700467
James E. Blaire511d2f2016-12-08 15:22:26 -0800468 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700469 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800470 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000471 connection_config)
472
Monty Taylorb934c1a2017-06-16 19:31:47 -0500473 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700474 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
475 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000476 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700477 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200478 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700479
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700480 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700481 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700482 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700483 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700484 c = FakeGerritChange(self, self.change_number, project, branch,
485 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700486 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700487 self.changes[self.change_number] = c
488 return c
489
James E. Blair1edfd972017-12-01 15:54:24 -0800490 def addFakeTag(self, project, branch, tag):
491 path = os.path.join(self.upstream_root, project)
492 repo = git.Repo(path)
493 commit = repo.heads[branch].commit
494 newrev = commit.hexsha
495 ref = 'refs/tags/' + tag
496
497 git.Tag.create(repo, tag, commit)
498
499 event = {
500 "type": "ref-updated",
501 "submitter": {
502 "name": "User Name",
503 },
504 "refUpdate": {
505 "oldRev": 40 * '0',
506 "newRev": newrev,
507 "refName": ref,
508 "project": project,
509 }
510 }
511 return event
512
James E. Blair72facdc2017-08-17 10:29:12 -0700513 def getFakeBranchCreatedEvent(self, project, branch):
514 path = os.path.join(self.upstream_root, project)
515 repo = git.Repo(path)
516 oldrev = 40 * '0'
517
518 event = {
519 "type": "ref-updated",
520 "submitter": {
521 "name": "User Name",
522 },
523 "refUpdate": {
524 "oldRev": oldrev,
525 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700526 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700527 "project": project,
528 }
529 }
530 return event
531
Clark Boylanb640e052014-04-03 16:41:46 -0700532 def review(self, project, changeid, message, action):
533 number, ps = changeid.split(',')
534 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000535
536 # Add the approval back onto the change (ie simulate what gerrit would
537 # do).
538 # Usually when zuul leaves a review it'll create a feedback loop where
539 # zuul's review enters another gerrit event (which is then picked up by
540 # zuul). However, we can't mimic this behaviour (by adding this
541 # approval event into the queue) as it stops jobs from checking what
542 # happens before this event is triggered. If a job needs to see what
543 # happens they can add their own verified event into the queue.
544 # Nevertheless, we can update change with the new review in gerrit.
545
James E. Blair8b5408c2016-08-08 15:37:46 -0700546 for cat in action.keys():
547 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000548 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000549
Clark Boylanb640e052014-04-03 16:41:46 -0700550 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000551
Clark Boylanb640e052014-04-03 16:41:46 -0700552 if 'submit' in action:
553 change.setMerged()
554 if message:
555 change.setReported()
556
557 def query(self, number):
558 change = self.changes.get(int(number))
559 if change:
560 return change.query()
561 return {}
562
James E. Blair0e4c7912018-01-02 14:20:17 -0800563 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800564 if query.startswith('change:'):
565 # Query a specific changeid
566 changeid = query[len('change:'):]
567 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800568 if (change.data['id'] == changeid or
569 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700570 elif query.startswith('message:'):
571 # Query the content of a commit message
572 msg = query[len('message:'):].strip()
573 l = [change.query() for change in self.changes.values()
574 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800575 else:
576 # Query all open changes
577 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700578 return l
James E. Blairc494d542014-08-06 09:23:52 -0700579
James E. Blair0e4c7912018-01-02 14:20:17 -0800580 def simpleQuery(self, query):
581 self.log.debug("simpleQuery: %s" % query)
582 self.queries.append(query)
583 results = []
584 if query.startswith('(') and 'OR' in query:
585 query = query[1:-2]
586 for q in query.split(' OR '):
587 for r in self._simpleQuery(q):
588 if r not in results:
589 results.append(r)
590 else:
591 results = self._simpleQuery(query)
592 return results
593
Joshua Hesketh352264b2015-08-11 23:42:08 +1000594 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700595 pass
596
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200597 def _uploadPack(self, project):
598 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
599 'multi_ack thin-pack side-band side-band-64k ofs-delta '
600 'shallow no-progress include-tag multi_ack_detailed no-done\n')
601 path = os.path.join(self.upstream_root, project.name)
602 repo = git.Repo(path)
603 for ref in repo.refs:
604 r = ref.object.hexsha + ' ' + ref.path + '\n'
605 ret += '%04x%s' % (len(r) + 4, r)
606 ret += '0000'
607 return ret
608
Joshua Hesketh352264b2015-08-11 23:42:08 +1000609 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800610 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000611
Clark Boylanb640e052014-04-03 16:41:46 -0700612
Gregory Haynes4fc12542015-04-22 20:38:06 -0700613class GithubChangeReference(git.Reference):
614 _common_path_default = "refs/pull"
615 _points_to_commits_only = True
616
617
618class FakeGithubPullRequest(object):
619
620 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800621 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700622 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700623 """Creates a new PR with several commits.
624 Sends an event about opened PR."""
625 self.github = github
626 self.source = github
627 self.number = number
628 self.project = project
629 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100630 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700631 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100632 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700633 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100634 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700635 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100636 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100637 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800638 self.reviews = []
639 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700640 self.updated_at = None
641 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100642 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100643 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700644 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800645 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700646 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100647 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700648 self._updateTimeStamp()
649
Jan Hruban570d01c2016-03-10 21:51:32 +0100650 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700651 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100652 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700653 self._updateTimeStamp()
654
Jan Hruban570d01c2016-03-10 21:51:32 +0100655 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700656 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100657 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700658 self._updateTimeStamp()
659
660 def getPullRequestOpenedEvent(self):
661 return self._getPullRequestEvent('opened')
662
663 def getPullRequestSynchronizeEvent(self):
664 return self._getPullRequestEvent('synchronize')
665
666 def getPullRequestReopenedEvent(self):
667 return self._getPullRequestEvent('reopened')
668
669 def getPullRequestClosedEvent(self):
670 return self._getPullRequestEvent('closed')
671
Jesse Keatinga41566f2017-06-14 18:17:51 -0700672 def getPullRequestEditedEvent(self):
673 return self._getPullRequestEvent('edited')
674
Gregory Haynes4fc12542015-04-22 20:38:06 -0700675 def addComment(self, message):
676 self.comments.append(message)
677 self._updateTimeStamp()
678
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200679 def getCommentAddedEvent(self, text):
680 name = 'issue_comment'
681 data = {
682 'action': 'created',
683 'issue': {
684 'number': self.number
685 },
686 'comment': {
687 'body': text
688 },
689 'repository': {
690 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100691 },
692 'sender': {
693 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200694 }
695 }
696 return (name, data)
697
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800698 def getReviewAddedEvent(self, review):
699 name = 'pull_request_review'
700 data = {
701 'action': 'submitted',
702 'pull_request': {
703 'number': self.number,
704 'title': self.subject,
705 'updated_at': self.updated_at,
706 'base': {
707 'ref': self.branch,
708 'repo': {
709 'full_name': self.project
710 }
711 },
712 'head': {
713 'sha': self.head_sha
714 }
715 },
716 'review': {
717 'state': review
718 },
719 'repository': {
720 'full_name': self.project
721 },
722 'sender': {
723 'login': 'ghuser'
724 }
725 }
726 return (name, data)
727
Jan Hruban16ad31f2015-11-07 14:39:07 +0100728 def addLabel(self, name):
729 if name not in self.labels:
730 self.labels.append(name)
731 self._updateTimeStamp()
732 return self._getLabelEvent(name)
733
734 def removeLabel(self, name):
735 if name in self.labels:
736 self.labels.remove(name)
737 self._updateTimeStamp()
738 return self._getUnlabelEvent(name)
739
740 def _getLabelEvent(self, label):
741 name = 'pull_request'
742 data = {
743 'action': 'labeled',
744 'pull_request': {
745 'number': self.number,
746 'updated_at': self.updated_at,
747 'base': {
748 'ref': self.branch,
749 'repo': {
750 'full_name': self.project
751 }
752 },
753 'head': {
754 'sha': self.head_sha
755 }
756 },
757 'label': {
758 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100759 },
760 'sender': {
761 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100762 }
763 }
764 return (name, data)
765
766 def _getUnlabelEvent(self, label):
767 name = 'pull_request'
768 data = {
769 'action': 'unlabeled',
770 'pull_request': {
771 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100772 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100773 'updated_at': self.updated_at,
774 'base': {
775 'ref': self.branch,
776 'repo': {
777 'full_name': self.project
778 }
779 },
780 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800781 'sha': self.head_sha,
782 'repo': {
783 'full_name': self.project
784 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100785 }
786 },
787 'label': {
788 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100789 },
790 'sender': {
791 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100792 }
793 }
794 return (name, data)
795
Jesse Keatinga41566f2017-06-14 18:17:51 -0700796 def editBody(self, body):
797 self.body = body
798 self._updateTimeStamp()
799
Gregory Haynes4fc12542015-04-22 20:38:06 -0700800 def _getRepo(self):
801 repo_path = os.path.join(self.upstream_root, self.project)
802 return git.Repo(repo_path)
803
804 def _createPRRef(self):
805 repo = self._getRepo()
806 GithubChangeReference.create(
807 repo, self._getPRReference(), 'refs/tags/init')
808
Jan Hruban570d01c2016-03-10 21:51:32 +0100809 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700810 repo = self._getRepo()
811 ref = repo.references[self._getPRReference()]
812 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100813 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700814 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100815 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700816 repo.head.reference = ref
817 zuul.merger.merger.reset_repo_to_head(repo)
818 repo.git.clean('-x', '-f', '-d')
819
Jan Hruban570d01c2016-03-10 21:51:32 +0100820 if files:
821 fn = files[0]
822 self.files = files
823 else:
824 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
825 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100826 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700827 fn = os.path.join(repo.working_dir, fn)
828 f = open(fn, 'w')
829 with open(fn, 'w') as f:
830 f.write("test %s %s\n" %
831 (self.branch, self.number))
832 repo.index.add([fn])
833
834 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800835 # Create an empty set of statuses for the given sha,
836 # each sha on a PR may have a status set on it
837 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700838 repo.head.reference = 'master'
839 zuul.merger.merger.reset_repo_to_head(repo)
840 repo.git.clean('-x', '-f', '-d')
841 repo.heads['master'].checkout()
842
843 def _updateTimeStamp(self):
844 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
845
846 def getPRHeadSha(self):
847 repo = self._getRepo()
848 return repo.references[self._getPRReference()].commit.hexsha
849
Jesse Keatingae4cd272017-01-30 17:10:44 -0800850 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800851 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
852 # convert the timestamp to a str format that would be returned
853 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800854
Adam Gandelmand81dd762017-02-09 15:15:49 -0800855 if granted_on:
856 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
857 submitted_at = time.strftime(
858 gh_time_format, granted_on.timetuple())
859 else:
860 # github timestamps only down to the second, so we need to make
861 # sure reviews that tests add appear to be added over a period of
862 # time in the past and not all at once.
863 if not self.reviews:
864 # the first review happens 10 mins ago
865 offset = 600
866 else:
867 # subsequent reviews happen 1 minute closer to now
868 offset = 600 - (len(self.reviews) * 60)
869
870 granted_on = datetime.datetime.utcfromtimestamp(
871 time.time() - offset)
872 submitted_at = time.strftime(
873 gh_time_format, granted_on.timetuple())
874
Jesse Keatingae4cd272017-01-30 17:10:44 -0800875 self.reviews.append({
876 'state': state,
877 'user': {
878 'login': user,
879 'email': user + "@derp.com",
880 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800881 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800882 })
883
Gregory Haynes4fc12542015-04-22 20:38:06 -0700884 def _getPRReference(self):
885 return '%s/head' % self.number
886
887 def _getPullRequestEvent(self, action):
888 name = 'pull_request'
889 data = {
890 'action': action,
891 'number': self.number,
892 'pull_request': {
893 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100894 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700895 'updated_at': self.updated_at,
896 'base': {
897 'ref': self.branch,
898 'repo': {
899 'full_name': self.project
900 }
901 },
902 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800903 'sha': self.head_sha,
904 'repo': {
905 'full_name': self.project
906 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700907 },
908 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100909 },
910 'sender': {
911 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700912 }
913 }
914 return (name, data)
915
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800916 def getCommitStatusEvent(self, context, state='success', user='zuul'):
917 name = 'status'
918 data = {
919 'state': state,
920 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700921 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800922 'description': 'Test results for %s: %s' % (self.head_sha, state),
923 'target_url': 'http://zuul/%s' % self.head_sha,
924 'branches': [],
925 'context': context,
926 'sender': {
927 'login': user
928 }
929 }
930 return (name, data)
931
James E. Blair289f5932017-07-27 15:02:29 -0700932 def setMerged(self, commit_message):
933 self.is_merged = True
934 self.merge_message = commit_message
935
936 repo = self._getRepo()
937 repo.heads[self.branch].commit = repo.commit(self.head_sha)
938
Gregory Haynes4fc12542015-04-22 20:38:06 -0700939
940class FakeGithubConnection(githubconnection.GithubConnection):
941 log = logging.getLogger("zuul.test.FakeGithubConnection")
942
Jesse Keating80730e62017-09-14 15:35:11 -0600943 def __init__(self, driver, connection_name, connection_config, rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -0800944 changes_db=None, upstream_root=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700945 super(FakeGithubConnection, self).__init__(driver, connection_name,
946 connection_config)
947 self.connection_name = connection_name
948 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800949 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700950 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700951 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100952 self.merge_failure = False
953 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100954 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800955 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Jesse Keating80730e62017-09-14 15:35:11 -0600956 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200957
958 def getGithubClient(self,
959 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600960 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200961 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700962
Jesse Keating80730e62017-09-14 15:35:11 -0600963 def setZuulWebPort(self, port):
964 self.zuul_web_port = port
965
Jesse Keatinga41566f2017-06-14 18:17:51 -0700966 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700967 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700968 self.pr_number += 1
969 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100970 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700971 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800972 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700973 return pull_request
974
Jesse Keating71a47ff2017-06-06 11:36:43 -0700975 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
976 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700977 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700978 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700979 if not new_rev:
980 new_rev = random_sha1()
981 name = 'push'
982 data = {
983 'ref': ref,
984 'before': old_rev,
985 'after': new_rev,
986 'repository': {
987 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700988 },
989 'commits': [
990 {
991 'added': added_files,
992 'removed': removed_files,
993 'modified': modified_files
994 }
995 ]
Wayne1a78c612015-06-11 17:14:13 -0700996 }
997 return (name, data)
998
Jesse Keating80730e62017-09-14 15:35:11 -0600999 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001000 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001002 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001003 secret = self.connection_config['webhook_token']
1004 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001005 headers = {'x-github-event': name, 'x-hub-signature': signature}
1006
1007 if use_zuulweb:
1008 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001009 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001010 % (self.zuul_web_port, self.connection_name),
1011 data=payload, headers=headers)
1012 return urllib.request.urlopen(req)
1013 else:
1014 job = self.rpcclient.submitJob(
1015 'github:%s:payload' % self.connection_name,
1016 {'headers': headers, 'body': data})
1017 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001019 def addProject(self, project):
1020 # use the original method here and additionally register it in the
1021 # fake github
1022 super(FakeGithubConnection, self).addProject(project)
1023 self.getGithubClient(project).addProject(project)
1024
Jesse Keating9021a012017-08-29 14:45:27 -07001025 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001026 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001027 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001028 if len(prs) > 1:
1029 raise Exception('Multiple pulls found with head sha: %s' % sha)
1030 pr = prs[0]
1031 return self.getPull(pr.project, pr.number)
1032
Jesse Keatingae4cd272017-01-30 17:10:44 -08001033 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001034 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001035 return pr.reviews
1036
Jesse Keatingae4cd272017-01-30 17:10:44 -08001037 def getRepoPermission(self, project, login):
1038 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001039 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001040 pr_owner, pr_project = pr.project.split('/')
1041 if (pr_owner == owner and proj == pr_project):
1042 if login in pr.writers:
1043 return 'write'
1044 else:
1045 return 'read'
1046
Gregory Haynes4fc12542015-04-22 20:38:06 -07001047 def getGitUrl(self, project):
1048 return os.path.join(self.upstream_root, str(project))
1049
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001050 def real_getGitUrl(self, project):
1051 return super(FakeGithubConnection, self).getGitUrl(project)
1052
Jan Hrubane252a732017-01-03 15:03:09 +01001053 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001054 # record that this got reported
1055 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001056 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001057 pull_request.addComment(message)
1058
Jan Hruban3b415922016-02-03 13:10:22 +01001059 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001060 # record that this got reported
1061 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001062 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001063 if self.merge_failure:
1064 raise Exception('Pull request was not merged')
1065 if self.merge_not_allowed_count > 0:
1066 self.merge_not_allowed_count -= 1
1067 raise MergeFailure('Merge was not successful due to mergeability'
1068 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001069 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001070
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001071 def setCommitStatus(self, project, sha, state, url='', description='',
1072 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001073 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001074 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001075 super(FakeGithubConnection, self).setCommitStatus(
1076 project, sha, state,
1077 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001078
Jan Hruban16ad31f2015-11-07 14:39:07 +01001079 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001080 # record that this got reported
1081 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001082 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001083 pull_request.addLabel(label)
1084
1085 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001086 # record that this got reported
1087 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001088 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001089 pull_request.removeLabel(label)
1090
Gregory Haynes4fc12542015-04-22 20:38:06 -07001091
Clark Boylanb640e052014-04-03 16:41:46 -07001092class BuildHistory(object):
1093 def __init__(self, **kw):
1094 self.__dict__.update(kw)
1095
1096 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001097 return ("<Completed build, result: %s name: %s uuid: %s "
1098 "changes: %s ref: %s>" %
1099 (self.result, self.name, self.uuid,
1100 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001101
1102
Clark Boylanb640e052014-04-03 16:41:46 -07001103class FakeStatsd(threading.Thread):
1104 def __init__(self):
1105 threading.Thread.__init__(self)
1106 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001107 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001108 self.sock.bind(('', 0))
1109 self.port = self.sock.getsockname()[1]
1110 self.wake_read, self.wake_write = os.pipe()
1111 self.stats = []
1112
1113 def run(self):
1114 while True:
1115 poll = select.poll()
1116 poll.register(self.sock, select.POLLIN)
1117 poll.register(self.wake_read, select.POLLIN)
1118 ret = poll.poll()
1119 for (fd, event) in ret:
1120 if fd == self.sock.fileno():
1121 data = self.sock.recvfrom(1024)
1122 if not data:
1123 return
1124 self.stats.append(data[0])
1125 if fd == self.wake_read:
1126 return
1127
1128 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001129 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001130
1131
James E. Blaire1767bc2016-08-02 10:00:27 -07001132class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001133 log = logging.getLogger("zuul.test")
1134
Paul Belanger174a8272017-03-14 13:20:10 -04001135 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001137 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001138 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001139 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001140 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001141 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001142 # TODOv3(jeblair): self.node is really "the label of the node
1143 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001144 # keep using it like this, or we may end up exposing more of
1145 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001146 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001147 self.node = None
1148 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001149 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001150 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001151 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001152 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001153 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001154 self.wait_condition = threading.Condition()
1155 self.waiting = False
1156 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001157 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001158 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001159 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001160 items = self.parameters['zuul']['items']
1161 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1162 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001163
James E. Blair3158e282016-08-19 09:34:11 -07001164 def __repr__(self):
1165 waiting = ''
1166 if self.waiting:
1167 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001168 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1169 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001170
Clark Boylanb640e052014-04-03 16:41:46 -07001171 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001172 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001173 self.wait_condition.acquire()
1174 self.wait_condition.notify()
1175 self.waiting = False
1176 self.log.debug("Build %s released" % self.unique)
1177 self.wait_condition.release()
1178
1179 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001180 """Return whether this build is being held.
1181
1182 :returns: Whether the build is being held.
1183 :rtype: bool
1184 """
1185
Clark Boylanb640e052014-04-03 16:41:46 -07001186 self.wait_condition.acquire()
1187 if self.waiting:
1188 ret = True
1189 else:
1190 ret = False
1191 self.wait_condition.release()
1192 return ret
1193
1194 def _wait(self):
1195 self.wait_condition.acquire()
1196 self.waiting = True
1197 self.log.debug("Build %s waiting" % self.unique)
1198 self.wait_condition.wait()
1199 self.wait_condition.release()
1200
1201 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001202 self.log.debug('Running build %s' % self.unique)
1203
Paul Belanger174a8272017-03-14 13:20:10 -04001204 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001205 self.log.debug('Holding build %s' % self.unique)
1206 self._wait()
1207 self.log.debug("Build %s continuing" % self.unique)
1208
James E. Blair412fba82017-01-26 15:00:50 -08001209 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001210 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001211 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001212 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001213 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001214 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001215 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001216
James E. Blaire1767bc2016-08-02 10:00:27 -07001217 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001218
James E. Blaira5dba232016-08-08 15:53:24 -07001219 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001220 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001221 for change in changes:
1222 if self.hasChanges(change):
1223 return True
1224 return False
1225
James E. Blaire7b99a02016-08-05 14:27:34 -07001226 def hasChanges(self, *changes):
1227 """Return whether this build has certain changes in its git repos.
1228
1229 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001230 are expected to be present (in order) in the git repository of
1231 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001232
1233 :returns: Whether the build has the indicated changes.
1234 :rtype: bool
1235
1236 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001237 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001238 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001239 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001240 try:
1241 repo = git.Repo(path)
1242 except NoSuchPathError as e:
1243 self.log.debug('%s' % e)
1244 return False
James E. Blair247cab72017-07-20 16:52:36 -07001245 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001246 commit_message = '%s-1' % change.subject
1247 self.log.debug("Checking if build %s has changes; commit_message "
1248 "%s; repo_messages %s" % (self, commit_message,
1249 repo_messages))
1250 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001251 self.log.debug(" messages do not match")
1252 return False
1253 self.log.debug(" OK")
1254 return True
1255
James E. Blaird8af5422017-05-24 13:59:40 -07001256 def getWorkspaceRepos(self, projects):
1257 """Return workspace git repo objects for the listed projects
1258
1259 :arg list projects: A list of strings, each the canonical name
1260 of a project.
1261
1262 :returns: A dictionary of {name: repo} for every listed
1263 project.
1264 :rtype: dict
1265
1266 """
1267
1268 repos = {}
1269 for project in projects:
1270 path = os.path.join(self.jobdir.src_root, project)
1271 repo = git.Repo(path)
1272 repos[project] = repo
1273 return repos
1274
Clark Boylanb640e052014-04-03 16:41:46 -07001275
James E. Blair107bb252017-10-13 15:53:16 -07001276class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1277 def doMergeChanges(self, merger, items, repo_state):
1278 # Get a merger in order to update the repos involved in this job.
1279 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1280 merger, items, repo_state)
1281 if not commit: # merge conflict
1282 self.recordResult('MERGER_FAILURE')
1283 return commit
1284
1285 def recordResult(self, result):
1286 build = self.executor_server.job_builds[self.job.unique]
1287 self.executor_server.lock.acquire()
1288 self.executor_server.build_history.append(
1289 BuildHistory(name=build.name, result=result, changes=build.changes,
1290 node=build.node, uuid=build.unique,
1291 ref=build.parameters['zuul']['ref'],
1292 parameters=build.parameters, jobdir=build.jobdir,
1293 pipeline=build.parameters['zuul']['pipeline'])
1294 )
1295 self.executor_server.running_builds.remove(build)
1296 del self.executor_server.job_builds[self.job.unique]
1297 self.executor_server.lock.release()
1298
1299 def runPlaybooks(self, args):
1300 build = self.executor_server.job_builds[self.job.unique]
1301 build.jobdir = self.jobdir
1302
1303 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1304 self.recordResult(result)
1305 return result
1306
James E. Blaira86aaf12017-10-15 20:59:50 -07001307 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001308 build = self.executor_server.job_builds[self.job.unique]
1309
1310 if self.executor_server._run_ansible:
1311 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001312 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001313 else:
1314 if playbook.path:
1315 result = build.run()
1316 else:
1317 result = (self.RESULT_NORMAL, 0)
1318 return result
1319
1320 def getHostList(self, args):
1321 self.log.debug("hostlist")
1322 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1323 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001324 if not host['host_vars'].get('ansible_connection'):
1325 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001326
1327 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001328 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001329 host_vars=dict(ansible_connection='local'),
1330 host_keys=[]))
1331 return hosts
1332
1333
Paul Belanger174a8272017-03-14 13:20:10 -04001334class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1335 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001336
Paul Belanger174a8272017-03-14 13:20:10 -04001337 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001338 they will report that they have started but then pause until
1339 released before reporting completion. This attribute may be
1340 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001341 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001342 be explicitly released.
1343
1344 """
James E. Blairfaf81982017-10-10 15:42:26 -07001345
1346 _job_class = RecordingAnsibleJob
1347
James E. Blairf5dbd002015-12-23 15:26:17 -08001348 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001349 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001350 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001351 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001352 self.hold_jobs_in_build = False
1353 self.lock = threading.Lock()
1354 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001355 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001357 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001358
James E. Blaira5dba232016-08-08 15:53:24 -07001359 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001360 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001361
1362 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001363 :arg Change change: The :py:class:`~tests.base.FakeChange`
1364 instance which should cause the job to fail. This job
1365 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001366
1367 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001368 l = self.fail_tests.get(name, [])
1369 l.append(change)
1370 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001371
James E. Blair962220f2016-08-03 11:22:38 -07001372 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001373 """Release a held build.
1374
1375 :arg str regex: A regular expression which, if supplied, will
1376 cause only builds with matching names to be released. If
1377 not supplied, all builds will be released.
1378
1379 """
James E. Blair962220f2016-08-03 11:22:38 -07001380 builds = self.running_builds[:]
1381 self.log.debug("Releasing build %s (%s)" % (regex,
1382 len(self.running_builds)))
1383 for build in builds:
1384 if not regex or re.match(regex, build.name):
1385 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001386 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001387 build.release()
1388 else:
1389 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001390 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001391 self.log.debug("Done releasing builds %s (%s)" %
1392 (regex, len(self.running_builds)))
1393
Paul Belanger174a8272017-03-14 13:20:10 -04001394 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001395 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001397 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001398 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001399 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001400 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001401 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001402 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001403
1404 def stopJob(self, job):
1405 self.log.debug("handle stop")
1406 parameters = json.loads(job.arguments)
1407 uuid = parameters['uuid']
1408 for build in self.running_builds:
1409 if build.unique == uuid:
1410 build.aborted = True
1411 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001412 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001413
James E. Blaira002b032017-04-18 10:35:48 -07001414 def stop(self):
1415 for build in self.running_builds:
1416 build.release()
1417 super(RecordingExecutorServer, self).stop()
1418
Joshua Hesketh50c21782016-10-13 21:34:14 +11001419
Clark Boylanb640e052014-04-03 16:41:46 -07001420class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001421 """A Gearman server for use in tests.
1422
1423 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1424 added to the queue but will not be distributed to workers
1425 until released. This attribute may be changed at any time and
1426 will take effect for subsequently enqueued jobs, but
1427 previously held jobs will still need to be explicitly
1428 released.
1429
1430 """
1431
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001432 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001433 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001434 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001435 if use_ssl:
1436 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1437 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1438 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1439 else:
1440 ssl_ca = None
1441 ssl_cert = None
1442 ssl_key = None
1443
1444 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1445 ssl_cert=ssl_cert,
1446 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001447
1448 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001449 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1450 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001451 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001452 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001453 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001454 elif job.name.startswith(b'merger:'):
1455 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001456 else:
1457 job.waiting = False
1458 if job.waiting:
1459 continue
1460 if job.name in connection.functions:
1461 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001462 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001463 connection.related_jobs[job.handle] = job
1464 job.worker_connection = connection
1465 job.running = True
1466 return job
1467 return None
1468
1469 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001470 """Release a held job.
1471
1472 :arg str regex: A regular expression which, if supplied, will
1473 cause only jobs with matching names to be released. If
1474 not supplied, all jobs will be released.
1475 """
Clark Boylanb640e052014-04-03 16:41:46 -07001476 released = False
1477 qlen = (len(self.high_queue) + len(self.normal_queue) +
1478 len(self.low_queue))
1479 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1480 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001481 match = False
1482 if job.name == b'executor:execute':
1483 parameters = json.loads(job.arguments.decode('utf8'))
1484 if not regex or re.match(regex, parameters.get('job')):
1485 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001486 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001487 if not regex:
1488 match = True
1489 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001490 self.log.debug("releasing queued job %s" %
1491 job.unique)
1492 job.waiting = False
1493 released = True
1494 else:
1495 self.log.debug("not releasing queued job %s" %
1496 job.unique)
1497 if released:
1498 self.wakeConnections()
1499 qlen = (len(self.high_queue) + len(self.normal_queue) +
1500 len(self.low_queue))
1501 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1502
1503
1504class FakeSMTP(object):
1505 log = logging.getLogger('zuul.FakeSMTP')
1506
1507 def __init__(self, messages, server, port):
1508 self.server = server
1509 self.port = port
1510 self.messages = messages
1511
1512 def sendmail(self, from_email, to_email, msg):
1513 self.log.info("Sending email from %s, to %s, with msg %s" % (
1514 from_email, to_email, msg))
1515
1516 headers = msg.split('\n\n', 1)[0]
1517 body = msg.split('\n\n', 1)[1]
1518
1519 self.messages.append(dict(
1520 from_email=from_email,
1521 to_email=to_email,
1522 msg=msg,
1523 headers=headers,
1524 body=body,
1525 ))
1526
1527 return True
1528
1529 def quit(self):
1530 return True
1531
1532
James E. Blairdce6cea2016-12-20 16:45:32 -08001533class FakeNodepool(object):
1534 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001535 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001536
1537 log = logging.getLogger("zuul.test.FakeNodepool")
1538
1539 def __init__(self, host, port, chroot):
1540 self.client = kazoo.client.KazooClient(
1541 hosts='%s:%s%s' % (host, port, chroot))
1542 self.client.start()
1543 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001544 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001545 self.thread = threading.Thread(target=self.run)
1546 self.thread.daemon = True
1547 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001548 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001549
1550 def stop(self):
1551 self._running = False
1552 self.thread.join()
1553 self.client.stop()
1554 self.client.close()
1555
1556 def run(self):
1557 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001558 try:
1559 self._run()
1560 except Exception:
1561 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001562 time.sleep(0.1)
1563
1564 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001565 if self.paused:
1566 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001567 for req in self.getNodeRequests():
1568 self.fulfillRequest(req)
1569
1570 def getNodeRequests(self):
1571 try:
1572 reqids = self.client.get_children(self.REQUEST_ROOT)
1573 except kazoo.exceptions.NoNodeError:
1574 return []
1575 reqs = []
1576 for oid in sorted(reqids):
1577 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001578 try:
1579 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001580 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001581 data['_oid'] = oid
1582 reqs.append(data)
1583 except kazoo.exceptions.NoNodeError:
1584 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001585 return reqs
1586
James E. Blaire18d4602017-01-05 11:17:28 -08001587 def getNodes(self):
1588 try:
1589 nodeids = self.client.get_children(self.NODE_ROOT)
1590 except kazoo.exceptions.NoNodeError:
1591 return []
1592 nodes = []
1593 for oid in sorted(nodeids):
1594 path = self.NODE_ROOT + '/' + oid
1595 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001596 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001597 data['_oid'] = oid
1598 try:
1599 lockfiles = self.client.get_children(path + '/lock')
1600 except kazoo.exceptions.NoNodeError:
1601 lockfiles = []
1602 if lockfiles:
1603 data['_lock'] = True
1604 else:
1605 data['_lock'] = False
1606 nodes.append(data)
1607 return nodes
1608
James E. Blaira38c28e2017-01-04 10:33:20 -08001609 def makeNode(self, request_id, node_type):
1610 now = time.time()
1611 path = '/nodepool/nodes/'
1612 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001613 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001614 provider='test-provider',
1615 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001616 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001617 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001618 public_ipv4='127.0.0.1',
1619 private_ipv4=None,
1620 public_ipv6=None,
1621 allocated_to=request_id,
1622 state='ready',
1623 state_time=now,
1624 created_time=now,
1625 updated_time=now,
1626 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001627 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001628 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001629 if 'fakeuser' in node_type:
1630 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001631 if 'windows' in node_type:
1632 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001633 if 'network' in node_type:
1634 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001635
Clint Byrumf322fe22017-05-10 20:53:12 -07001636 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001637 path = self.client.create(path, data,
1638 makepath=True,
1639 sequence=True)
1640 nodeid = path.split("/")[-1]
1641 return nodeid
1642
James E. Blair6ab79e02017-01-06 10:10:17 -08001643 def addFailRequest(self, request):
1644 self.fail_requests.add(request['_oid'])
1645
James E. Blairdce6cea2016-12-20 16:45:32 -08001646 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001647 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001648 return
1649 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001650 oid = request['_oid']
1651 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001652
James E. Blair6ab79e02017-01-06 10:10:17 -08001653 if oid in self.fail_requests:
1654 request['state'] = 'failed'
1655 else:
1656 request['state'] = 'fulfilled'
1657 nodes = []
1658 for node in request['node_types']:
1659 nodeid = self.makeNode(oid, node)
1660 nodes.append(nodeid)
1661 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001662
James E. Blaira38c28e2017-01-04 10:33:20 -08001663 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001664 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001665 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001666 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001667 try:
1668 self.client.set(path, data)
1669 except kazoo.exceptions.NoNodeError:
1670 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001671
1672
James E. Blair498059b2016-12-20 13:50:13 -08001673class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001674 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001675 super(ChrootedKazooFixture, self).__init__()
1676
1677 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1678 if ':' in zk_host:
1679 host, port = zk_host.split(':')
1680 else:
1681 host = zk_host
1682 port = None
1683
1684 self.zookeeper_host = host
1685
1686 if not port:
1687 self.zookeeper_port = 2181
1688 else:
1689 self.zookeeper_port = int(port)
1690
Clark Boylan621ec9a2017-04-07 17:41:33 -07001691 self.test_id = test_id
1692
James E. Blair498059b2016-12-20 13:50:13 -08001693 def _setUp(self):
1694 # Make sure the test chroot paths do not conflict
1695 random_bits = ''.join(random.choice(string.ascii_lowercase +
1696 string.ascii_uppercase)
1697 for x in range(8))
1698
Clark Boylan621ec9a2017-04-07 17:41:33 -07001699 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001700 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1701
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001702 self.addCleanup(self._cleanup)
1703
James E. Blair498059b2016-12-20 13:50:13 -08001704 # Ensure the chroot path exists and clean up any pre-existing znodes.
1705 _tmp_client = kazoo.client.KazooClient(
1706 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1707 _tmp_client.start()
1708
1709 if _tmp_client.exists(self.zookeeper_chroot):
1710 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1711
1712 _tmp_client.ensure_path(self.zookeeper_chroot)
1713 _tmp_client.stop()
1714 _tmp_client.close()
1715
James E. Blair498059b2016-12-20 13:50:13 -08001716 def _cleanup(self):
1717 '''Remove the chroot path.'''
1718 # Need a non-chroot'ed client to remove the chroot path
1719 _tmp_client = kazoo.client.KazooClient(
1720 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1721 _tmp_client.start()
1722 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1723 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001724 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001725
1726
Joshua Heskethd78b4482015-09-14 16:56:34 -06001727class MySQLSchemaFixture(fixtures.Fixture):
1728 def setUp(self):
1729 super(MySQLSchemaFixture, self).setUp()
1730
1731 random_bits = ''.join(random.choice(string.ascii_lowercase +
1732 string.ascii_uppercase)
1733 for x in range(8))
1734 self.name = '%s_%s' % (random_bits, os.getpid())
1735 self.passwd = uuid.uuid4().hex
1736 db = pymysql.connect(host="localhost",
1737 user="openstack_citest",
1738 passwd="openstack_citest",
1739 db="openstack_citest")
1740 cur = db.cursor()
1741 cur.execute("create database %s" % self.name)
1742 cur.execute(
1743 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1744 (self.name, self.name, self.passwd))
1745 cur.execute("flush privileges")
1746
1747 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1748 self.passwd,
1749 self.name)
1750 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1751 self.addCleanup(self.cleanup)
1752
1753 def cleanup(self):
1754 db = pymysql.connect(host="localhost",
1755 user="openstack_citest",
1756 passwd="openstack_citest",
1757 db="openstack_citest")
1758 cur = db.cursor()
1759 cur.execute("drop database %s" % self.name)
1760 cur.execute("drop user '%s'@'localhost'" % self.name)
1761 cur.execute("flush privileges")
1762
1763
Maru Newby3fe5f852015-01-13 04:22:14 +00001764class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001765 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001766 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001767
James E. Blair1c236df2017-02-01 14:07:24 -08001768 def attachLogs(self, *args):
1769 def reader():
1770 self._log_stream.seek(0)
1771 while True:
1772 x = self._log_stream.read(4096)
1773 if not x:
1774 break
1775 yield x.encode('utf8')
1776 content = testtools.content.content_from_reader(
1777 reader,
1778 testtools.content_type.UTF8_TEXT,
1779 False)
1780 self.addDetail('logging', content)
1781
Clark Boylanb640e052014-04-03 16:41:46 -07001782 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001783 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001784 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1785 try:
1786 test_timeout = int(test_timeout)
1787 except ValueError:
1788 # If timeout value is invalid do not set a timeout.
1789 test_timeout = 0
1790 if test_timeout > 0:
1791 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1792
1793 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1794 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1795 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1796 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1797 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1798 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1799 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1800 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1801 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1802 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001803 self._log_stream = StringIO()
1804 self.addOnException(self.attachLogs)
1805 else:
1806 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001807
James E. Blair1c236df2017-02-01 14:07:24 -08001808 handler = logging.StreamHandler(self._log_stream)
1809 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1810 '%(levelname)-8s %(message)s')
1811 handler.setFormatter(formatter)
1812
1813 logger = logging.getLogger()
1814 logger.setLevel(logging.DEBUG)
1815 logger.addHandler(handler)
1816
Clark Boylan3410d532017-04-25 12:35:29 -07001817 # Make sure we don't carry old handlers around in process state
1818 # which slows down test runs
1819 self.addCleanup(logger.removeHandler, handler)
1820 self.addCleanup(handler.close)
1821 self.addCleanup(handler.flush)
1822
James E. Blair1c236df2017-02-01 14:07:24 -08001823 # NOTE(notmorgan): Extract logging overrides for specific
1824 # libraries from the OS_LOG_DEFAULTS env and create loggers
1825 # for each. This is used to limit the output during test runs
1826 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001827 log_defaults_from_env = os.environ.get(
1828 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001829 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001830
James E. Blairdce6cea2016-12-20 16:45:32 -08001831 if log_defaults_from_env:
1832 for default in log_defaults_from_env.split(','):
1833 try:
1834 name, level_str = default.split('=', 1)
1835 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001836 logger = logging.getLogger(name)
1837 logger.setLevel(level)
1838 logger.addHandler(handler)
1839 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001840 except ValueError:
1841 # NOTE(notmorgan): Invalid format of the log default,
1842 # skip and don't try and apply a logger for the
1843 # specified module
1844 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001845
Maru Newby3fe5f852015-01-13 04:22:14 +00001846
1847class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001848 """A test case with a functioning Zuul.
1849
1850 The following class variables are used during test setup and can
1851 be overidden by subclasses but are effectively read-only once a
1852 test method starts running:
1853
1854 :cvar str config_file: This points to the main zuul config file
1855 within the fixtures directory. Subclasses may override this
1856 to obtain a different behavior.
1857
1858 :cvar str tenant_config_file: This is the tenant config file
1859 (which specifies from what git repos the configuration should
1860 be loaded). It defaults to the value specified in
1861 `config_file` but can be overidden by subclasses to obtain a
1862 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001863 configuration. See also the :py:func:`simple_layout`
1864 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001865
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001866 :cvar bool create_project_keys: Indicates whether Zuul should
1867 auto-generate keys for each project, or whether the test
1868 infrastructure should insert dummy keys to save time during
1869 startup. Defaults to False.
1870
James E. Blaire7b99a02016-08-05 14:27:34 -07001871 The following are instance variables that are useful within test
1872 methods:
1873
1874 :ivar FakeGerritConnection fake_<connection>:
1875 A :py:class:`~tests.base.FakeGerritConnection` will be
1876 instantiated for each connection present in the config file
1877 and stored here. For instance, `fake_gerrit` will hold the
1878 FakeGerritConnection object for a connection named `gerrit`.
1879
1880 :ivar FakeGearmanServer gearman_server: An instance of
1881 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1882 server that all of the Zuul components in this test use to
1883 communicate with each other.
1884
Paul Belanger174a8272017-03-14 13:20:10 -04001885 :ivar RecordingExecutorServer executor_server: An instance of
1886 :py:class:`~tests.base.RecordingExecutorServer` which is the
1887 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001888
1889 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1890 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001891 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001892 list upon completion.
1893
1894 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1895 objects representing completed builds. They are appended to
1896 the list in the order they complete.
1897
1898 """
1899
James E. Blair83005782015-12-11 14:46:03 -08001900 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001901 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001902 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001903 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001904
1905 def _startMerger(self):
1906 self.merge_server = zuul.merger.server.MergeServer(self.config,
1907 self.connections)
1908 self.merge_server.start()
1909
Maru Newby3fe5f852015-01-13 04:22:14 +00001910 def setUp(self):
1911 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001912
1913 self.setupZK()
1914
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001915 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001916 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001917 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1918 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001919 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001920 tmp_root = tempfile.mkdtemp(
1921 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001922 self.test_root = os.path.join(tmp_root, "zuul-test")
1923 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001924 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001925 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001926 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001927 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1928 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001929
1930 if os.path.exists(self.test_root):
1931 shutil.rmtree(self.test_root)
1932 os.makedirs(self.test_root)
1933 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001934 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001935 os.makedirs(self.merger_state_root)
1936 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001937
1938 # Make per test copy of Configuration.
1939 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001940 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1941 if not os.path.exists(self.private_key_file):
1942 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1943 shutil.copy(src_private_key_file, self.private_key_file)
1944 shutil.copy('{}.pub'.format(src_private_key_file),
1945 '{}.pub'.format(self.private_key_file))
1946 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001947 self.config.set('scheduler', 'tenant_config',
1948 os.path.join(
1949 FIXTURE_DIR,
1950 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001951 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001952 self.config.set(
1953 'scheduler', 'command_socket',
1954 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001955 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001956 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001957 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001958 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001959 self.config.set(
1960 'executor', 'command_socket',
1961 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001962 self.config.set(
1963 'merger', 'command_socket',
1964 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001965
Clark Boylanb640e052014-04-03 16:41:46 -07001966 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001967 if self.config.has_section('statsd'):
1968 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001969 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001970
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001971 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001972
1973 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001974 self.log.info("Gearman server on port %s" %
1975 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001976 if self.use_ssl:
1977 self.log.info('SSL enabled for gearman')
1978 self.config.set(
1979 'gearman', 'ssl_ca',
1980 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1981 self.config.set(
1982 'gearman', 'ssl_cert',
1983 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1984 self.config.set(
1985 'gearman', 'ssl_key',
1986 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001987
Jesse Keating80730e62017-09-14 15:35:11 -06001988 self.rpcclient = zuul.rpcclient.RPCClient(
1989 self.config.get('gearman', 'server'),
1990 self.gearman_server.port,
1991 get_default(self.config, 'gearman', 'ssl_key'),
1992 get_default(self.config, 'gearman', 'ssl_cert'),
1993 get_default(self.config, 'gearman', 'ssl_ca'))
1994
James E. Blaire511d2f2016-12-08 15:22:26 -08001995 gerritsource.GerritSource.replication_timeout = 1.5
1996 gerritsource.GerritSource.replication_retry_interval = 0.5
1997 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07001998
Joshua Hesketh352264b2015-08-11 23:42:08 +10001999 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002000 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002001
Jan Hruban6b71aff2015-10-22 16:58:08 +02002002 self.event_queues = [
2003 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002004 self.sched.trigger_event_queue,
2005 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002006 ]
2007
James E. Blairfef78942016-03-11 16:28:56 -08002008 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002009 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002010
Paul Belanger174a8272017-03-14 13:20:10 -04002011 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002012 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002013 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002014 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002015 _test_root=self.test_root,
2016 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002017 self.executor_server.start()
2018 self.history = self.executor_server.build_history
2019 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002020
Paul Belanger174a8272017-03-14 13:20:10 -04002021 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002022 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002023 self.merge_client = zuul.merger.client.MergeClient(
2024 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002025 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002026 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002027 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002028 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002029
James E. Blair0d5a36e2017-02-21 10:53:44 -05002030 self.fake_nodepool = FakeNodepool(
2031 self.zk_chroot_fixture.zookeeper_host,
2032 self.zk_chroot_fixture.zookeeper_port,
2033 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002034
Paul Belanger174a8272017-03-14 13:20:10 -04002035 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002036 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002037 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002038 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002039
Clark Boylanb640e052014-04-03 16:41:46 -07002040 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002041 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002042 # Cleanups are run in reverse order
2043 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002044 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002045 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002046
James E. Blairb9c0d772017-03-03 14:34:49 -08002047 self.sched.reconfigure(self.config)
2048 self.sched.resume()
2049
Tobias Henkel7df274b2017-05-26 17:41:11 +02002050 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002051 # Set up gerrit related fakes
2052 # Set a changes database so multiple FakeGerrit's can report back to
2053 # a virtual canonical database given by the configured hostname
2054 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002055 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002056
2057 def getGerritConnection(driver, name, config):
2058 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2059 con = FakeGerritConnection(driver, name, config,
2060 changes_db=db,
2061 upstream_root=self.upstream_root)
2062 self.event_queues.append(con.event_queue)
2063 setattr(self, 'fake_' + name, con)
2064 return con
2065
2066 self.useFixture(fixtures.MonkeyPatch(
2067 'zuul.driver.gerrit.GerritDriver.getConnection',
2068 getGerritConnection))
2069
Gregory Haynes4fc12542015-04-22 20:38:06 -07002070 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002071 server = config.get('server', 'github.com')
2072 db = self.github_changes_dbs.setdefault(server, {})
Gregory Haynes4fc12542015-04-22 20:38:06 -07002073 con = FakeGithubConnection(driver, name, config,
Jesse Keating80730e62017-09-14 15:35:11 -06002074 self.rpcclient,
James E. Blair6bacffb2018-01-05 13:45:25 -08002075 changes_db=db,
Gregory Haynes4fc12542015-04-22 20:38:06 -07002076 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002077 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002078 setattr(self, 'fake_' + name, con)
2079 return con
2080
2081 self.useFixture(fixtures.MonkeyPatch(
2082 'zuul.driver.github.GithubDriver.getConnection',
2083 getGithubConnection))
2084
James E. Blaire511d2f2016-12-08 15:22:26 -08002085 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002086 # TODO(jhesketh): This should come from lib.connections for better
2087 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002088 # Register connections from the config
2089 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002090
Joshua Hesketh352264b2015-08-11 23:42:08 +10002091 def FakeSMTPFactory(*args, **kw):
2092 args = [self.smtp_messages] + list(args)
2093 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002094
Joshua Hesketh352264b2015-08-11 23:42:08 +10002095 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002096
James E. Blaire511d2f2016-12-08 15:22:26 -08002097 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002098 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002099 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002100
James E. Blair83005782015-12-11 14:46:03 -08002101 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002102 # This creates the per-test configuration object. It can be
2103 # overriden by subclasses, but should not need to be since it
2104 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002105 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002106 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002107
James E. Blair39840362017-06-23 20:34:02 +01002108 sections = ['zuul', 'scheduler', 'executor', 'merger']
2109 for section in sections:
2110 if not self.config.has_section(section):
2111 self.config.add_section(section)
2112
James E. Blair06cc3922017-04-19 10:08:10 -07002113 if not self.setupSimpleLayout():
2114 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002115 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002116 self.tenant_config_file)
2117 git_path = os.path.join(
2118 os.path.dirname(
2119 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2120 'git')
2121 if os.path.exists(git_path):
2122 for reponame in os.listdir(git_path):
2123 project = reponame.replace('_', '/')
2124 self.copyDirToRepo(project,
2125 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002126 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002127 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002128 self.setupAllProjectKeys()
2129
James E. Blair06cc3922017-04-19 10:08:10 -07002130 def setupSimpleLayout(self):
2131 # If the test method has been decorated with a simple_layout,
2132 # use that instead of the class tenant_config_file. Set up a
2133 # single config-project with the specified layout, and
2134 # initialize repos for all of the 'project' entries which
2135 # appear in the layout.
2136 test_name = self.id().split('.')[-1]
2137 test = getattr(self, test_name)
2138 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002139 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002140 else:
2141 return False
2142
James E. Blairb70e55a2017-04-19 12:57:02 -07002143 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002144 path = os.path.join(FIXTURE_DIR, path)
2145 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002146 data = f.read()
2147 layout = yaml.safe_load(data)
2148 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002149 untrusted_projects = []
2150 for item in layout:
2151 if 'project' in item:
2152 name = item['project']['name']
2153 untrusted_projects.append(name)
2154 self.init_repo(name)
2155 self.addCommitToRepo(name, 'initial commit',
2156 files={'README': ''},
2157 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002158 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002159 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002160 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002161 for fn in zuul.configloader.as_list(
2162 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002163 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002164 for fn in zuul.configloader.as_list(
2165 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002166 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002167
2168 root = os.path.join(self.test_root, "config")
2169 if not os.path.exists(root):
2170 os.makedirs(root)
2171 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2172 config = [{'tenant':
2173 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002174 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002175 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002176 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002177 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002178 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002179 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002180 os.path.join(FIXTURE_DIR, f.name))
2181
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002182 self.init_repo('org/common-config')
2183 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002184 files, branch='master', tag='init')
2185
2186 return True
2187
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002188 def setupAllProjectKeys(self):
2189 if self.create_project_keys:
2190 return
2191
James E. Blair39840362017-06-23 20:34:02 +01002192 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002193 with open(os.path.join(FIXTURE_DIR, path)) as f:
2194 tenant_config = yaml.safe_load(f.read())
2195 for tenant in tenant_config:
2196 sources = tenant['tenant']['source']
2197 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002198 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002199 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002200 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 self.setupProjectKeys(source, project)
2202
2203 def setupProjectKeys(self, source, project):
2204 # Make sure we set up an RSA key for the project so that we
2205 # don't spend time generating one:
2206
James E. Blair6459db12017-06-29 14:57:20 -07002207 if isinstance(project, dict):
2208 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002209 key_root = os.path.join(self.state_root, 'keys')
2210 if not os.path.isdir(key_root):
2211 os.mkdir(key_root, 0o700)
2212 private_key_file = os.path.join(key_root, source, project + '.pem')
2213 private_key_dir = os.path.dirname(private_key_file)
2214 self.log.debug("Installing test keys for project %s at %s" % (
2215 project, private_key_file))
2216 if not os.path.isdir(private_key_dir):
2217 os.makedirs(private_key_dir)
2218 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2219 with open(private_key_file, 'w') as o:
2220 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002221
James E. Blair498059b2016-12-20 13:50:13 -08002222 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002223 self.zk_chroot_fixture = self.useFixture(
2224 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002225 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002226 self.zk_chroot_fixture.zookeeper_host,
2227 self.zk_chroot_fixture.zookeeper_port,
2228 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002229
James E. Blair96c6bf82016-01-15 16:20:40 -08002230 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002231 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002232
2233 files = {}
2234 for (dirpath, dirnames, filenames) in os.walk(source_path):
2235 for filename in filenames:
2236 test_tree_filepath = os.path.join(dirpath, filename)
2237 common_path = os.path.commonprefix([test_tree_filepath,
2238 source_path])
2239 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2240 with open(test_tree_filepath, 'r') as f:
2241 content = f.read()
2242 files[relative_filepath] = content
2243 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002244 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002245
James E. Blaire18d4602017-01-05 11:17:28 -08002246 def assertNodepoolState(self):
2247 # Make sure that there are no pending requests
2248
2249 requests = self.fake_nodepool.getNodeRequests()
2250 self.assertEqual(len(requests), 0)
2251
2252 nodes = self.fake_nodepool.getNodes()
2253 for node in nodes:
2254 self.assertFalse(node['_lock'], "Node %s is locked" %
2255 (node['_oid'],))
2256
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002257 def assertNoGeneratedKeys(self):
2258 # Make sure that Zuul did not generate any project keys
2259 # (unless it was supposed to).
2260
2261 if self.create_project_keys:
2262 return
2263
2264 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2265 test_key = i.read()
2266
2267 key_root = os.path.join(self.state_root, 'keys')
2268 for root, dirname, files in os.walk(key_root):
2269 for fn in files:
2270 with open(os.path.join(root, fn)) as f:
2271 self.assertEqual(test_key, f.read())
2272
Clark Boylanb640e052014-04-03 16:41:46 -07002273 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002274 self.log.debug("Assert final state")
2275 # Make sure no jobs are running
2276 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002277 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002278 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002279 gc.collect()
2280 for obj in gc.get_objects():
2281 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002282 self.log.debug("Leaked git repo object: 0x%x %s" %
2283 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002284 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002285 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002286 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002287 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002288 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002289 for tenant in self.sched.abide.tenants.values():
2290 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002291 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002292 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002293
2294 def shutdown(self):
2295 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002296 self.executor_server.hold_jobs_in_build = False
2297 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002298 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002299 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002300 if self.merge_server:
2301 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002302 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002303 self.sched.stop()
2304 self.sched.join()
2305 self.statsd.stop()
2306 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002307 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002308 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002309 self.fake_nodepool.stop()
2310 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002311 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002312 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002313 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002314 # Further the pydevd threads also need to be whitelisted so debugging
2315 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002316 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002317 'pydevd.CommandThread',
2318 'pydevd.Reader',
2319 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002320 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002321 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002322 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002323 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002324 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002325 log_str = ""
2326 for thread_id, stack_frame in sys._current_frames().items():
2327 log_str += "Thread: %s\n" % thread_id
2328 log_str += "".join(traceback.format_stack(stack_frame))
2329 self.log.debug(log_str)
2330 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002331
James E. Blaira002b032017-04-18 10:35:48 -07002332 def assertCleanShutdown(self):
2333 pass
2334
James E. Blairc4ba97a2017-04-19 16:26:24 -07002335 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002336 parts = project.split('/')
2337 path = os.path.join(self.upstream_root, *parts[:-1])
2338 if not os.path.exists(path):
2339 os.makedirs(path)
2340 path = os.path.join(self.upstream_root, project)
2341 repo = git.Repo.init(path)
2342
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002343 with repo.config_writer() as config_writer:
2344 config_writer.set_value('user', 'email', 'user@example.com')
2345 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002346
Clark Boylanb640e052014-04-03 16:41:46 -07002347 repo.index.commit('initial commit')
2348 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002349 if tag:
2350 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002351
James E. Blair97d902e2014-08-21 13:25:56 -07002352 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002353 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002354 repo.git.clean('-x', '-f', '-d')
2355
James E. Blair97d902e2014-08-21 13:25:56 -07002356 def create_branch(self, project, branch):
2357 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002358 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002359 fn = os.path.join(path, 'README')
2360
2361 branch_head = repo.create_head(branch)
2362 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002363 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002364 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002365 f.close()
2366 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002367 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002368
James E. Blair97d902e2014-08-21 13:25:56 -07002369 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002370 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002371 repo.git.clean('-x', '-f', '-d')
2372
James E. Blairda5bb7e2018-01-22 16:12:17 -08002373 def delete_branch(self, project, branch):
2374 path = os.path.join(self.upstream_root, project)
2375 repo = git.Repo(path)
2376 repo.head.reference = repo.heads['master']
2377 zuul.merger.merger.reset_repo_to_head(repo)
2378 repo.delete_head(repo.heads[branch], force=True)
2379
Sachi King9f16d522016-03-16 12:20:45 +11002380 def create_commit(self, project):
2381 path = os.path.join(self.upstream_root, project)
2382 repo = git.Repo(path)
2383 repo.head.reference = repo.heads['master']
2384 file_name = os.path.join(path, 'README')
2385 with open(file_name, 'a') as f:
2386 f.write('creating fake commit\n')
2387 repo.index.add([file_name])
2388 commit = repo.index.commit('Creating a fake commit')
2389 return commit.hexsha
2390
James E. Blairf4a5f022017-04-18 14:01:10 -07002391 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002392 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002393 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002394 while len(self.builds):
2395 self.release(self.builds[0])
2396 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002397 i += 1
2398 if count is not None and i >= count:
2399 break
James E. Blairb8c16472015-05-05 14:55:26 -07002400
James E. Blairdf25ddc2017-07-08 07:57:09 -07002401 def getSortedBuilds(self):
2402 "Return the list of currently running builds sorted by name"
2403
2404 return sorted(self.builds, key=lambda x: x.name)
2405
Clark Boylanb640e052014-04-03 16:41:46 -07002406 def release(self, job):
2407 if isinstance(job, FakeBuild):
2408 job.release()
2409 else:
2410 job.waiting = False
2411 self.log.debug("Queued job %s released" % job.unique)
2412 self.gearman_server.wakeConnections()
2413
2414 def getParameter(self, job, name):
2415 if isinstance(job, FakeBuild):
2416 return job.parameters[name]
2417 else:
2418 parameters = json.loads(job.arguments)
2419 return parameters[name]
2420
Clark Boylanb640e052014-04-03 16:41:46 -07002421 def haveAllBuildsReported(self):
2422 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002423 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002424 return False
2425 # Find out if every build that the worker has completed has been
2426 # reported back to Zuul. If it hasn't then that means a Gearman
2427 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002428 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002429 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002430 if not zbuild:
2431 # It has already been reported
2432 continue
2433 # It hasn't been reported yet.
2434 return False
2435 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002436 worker = self.executor_server.executor_worker
2437 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002438 if connection.state == 'GRAB_WAIT':
2439 return False
2440 return True
2441
2442 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002443 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002444 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002445 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002446 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002447 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002448 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002449 for j in conn.related_jobs.values():
2450 if j.unique == build.uuid:
2451 client_job = j
2452 break
2453 if not client_job:
2454 self.log.debug("%s is not known to the gearman client" %
2455 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002456 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002457 if not client_job.handle:
2458 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002459 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002460 server_job = self.gearman_server.jobs.get(client_job.handle)
2461 if not server_job:
2462 self.log.debug("%s is not known to the gearman server" %
2463 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002464 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002465 if not hasattr(server_job, 'waiting'):
2466 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002467 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002468 if server_job.waiting:
2469 continue
James E. Blair17302972016-08-10 16:11:42 -07002470 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002471 self.log.debug("%s has not reported start" % build)
2472 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002473 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002474 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002475 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002476 if worker_build:
2477 if worker_build.isWaiting():
2478 continue
2479 else:
2480 self.log.debug("%s is running" % worker_build)
2481 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002482 else:
James E. Blair962220f2016-08-03 11:22:38 -07002483 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002484 return False
James E. Blaira002b032017-04-18 10:35:48 -07002485 for (build_uuid, job_worker) in \
2486 self.executor_server.job_workers.items():
2487 if build_uuid not in seen_builds:
2488 self.log.debug("%s is not finalized" % build_uuid)
2489 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002490 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002491
James E. Blairdce6cea2016-12-20 16:45:32 -08002492 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002493 if self.fake_nodepool.paused:
2494 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002495 if self.sched.nodepool.requests:
2496 return False
2497 return True
2498
James E. Blaira615c362017-10-02 17:34:42 -07002499 def areAllMergeJobsWaiting(self):
2500 for client_job in list(self.merge_client.jobs):
2501 if not client_job.handle:
2502 self.log.debug("%s has no handle" % client_job)
2503 return False
2504 server_job = self.gearman_server.jobs.get(client_job.handle)
2505 if not server_job:
2506 self.log.debug("%s is not known to the gearman server" %
2507 client_job)
2508 return False
2509 if not hasattr(server_job, 'waiting'):
2510 self.log.debug("%s is being enqueued" % server_job)
2511 return False
2512 if server_job.waiting:
2513 self.log.debug("%s is waiting" % server_job)
2514 continue
2515 self.log.debug("%s is not waiting" % server_job)
2516 return False
2517 return True
2518
Jan Hruban6b71aff2015-10-22 16:58:08 +02002519 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002520 for event_queue in self.event_queues:
2521 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002522
2523 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002524 for event_queue in self.event_queues:
2525 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002526
Clark Boylanb640e052014-04-03 16:41:46 -07002527 def waitUntilSettled(self):
2528 self.log.debug("Waiting until settled...")
2529 start = time.time()
2530 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002531 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002532 self.log.error("Timeout waiting for Zuul to settle")
2533 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002534 for event_queue in self.event_queues:
2535 self.log.error(" %s: %s" %
2536 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002537 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002538 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002539 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002540 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002541 self.log.error("All requests completed: %s" %
2542 (self.areAllNodeRequestsComplete(),))
2543 self.log.error("Merge client jobs: %s" %
2544 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002545 raise Exception("Timeout waiting for Zuul to settle")
2546 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002547
Paul Belanger174a8272017-03-14 13:20:10 -04002548 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002549 # have all build states propogated to zuul?
2550 if self.haveAllBuildsReported():
2551 # Join ensures that the queue is empty _and_ events have been
2552 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002553 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002554 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002555 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002556 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002557 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002558 self.areAllNodeRequestsComplete() and
2559 all(self.eventQueuesEmpty())):
2560 # The queue empty check is placed at the end to
2561 # ensure that if a component adds an event between
2562 # when locked the run handler and checked that the
2563 # components were stable, we don't erroneously
2564 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002565 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002566 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002567 self.log.debug("...settled.")
2568 return
2569 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002570 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002571 self.sched.wake_event.wait(0.1)
2572
2573 def countJobResults(self, jobs, result):
2574 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002575 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002576
Monty Taylor0d926122017-05-24 08:07:56 -05002577 def getBuildByName(self, name):
2578 for build in self.builds:
2579 if build.name == name:
2580 return build
2581 raise Exception("Unable to find build %s" % name)
2582
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002583 def assertJobNotInHistory(self, name, project=None):
2584 for job in self.history:
2585 if (project is None or
2586 job.parameters['zuul']['project']['name'] == project):
2587 self.assertNotEqual(job.name, name,
2588 'Job %s found in history' % name)
2589
James E. Blair96c6bf82016-01-15 16:20:40 -08002590 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002591 for job in self.history:
2592 if (job.name == name and
2593 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002594 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002595 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002596 raise Exception("Unable to find job %s in history" % name)
2597
2598 def assertEmptyQueues(self):
2599 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002600 for tenant in self.sched.abide.tenants.values():
2601 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002602 for pipeline_queue in pipeline.queues:
2603 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002604 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002605 pipeline.name, pipeline_queue.name,
2606 pipeline_queue.queue))
2607 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002608 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002609
2610 def assertReportedStat(self, key, value=None, kind=None):
2611 start = time.time()
2612 while time.time() < (start + 5):
2613 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002614 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002615 if key == k:
2616 if value is None and kind is None:
2617 return
2618 elif value:
2619 if value == v:
2620 return
2621 elif kind:
2622 if v.endswith('|' + kind):
2623 return
2624 time.sleep(0.1)
2625
Clark Boylanb640e052014-04-03 16:41:46 -07002626 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002627
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002628 def assertBuilds(self, builds):
2629 """Assert that the running builds are as described.
2630
2631 The list of running builds is examined and must match exactly
2632 the list of builds described by the input.
2633
2634 :arg list builds: A list of dictionaries. Each item in the
2635 list must match the corresponding build in the build
2636 history, and each element of the dictionary must match the
2637 corresponding attribute of the build.
2638
2639 """
James E. Blair3158e282016-08-19 09:34:11 -07002640 try:
2641 self.assertEqual(len(self.builds), len(builds))
2642 for i, d in enumerate(builds):
2643 for k, v in d.items():
2644 self.assertEqual(
2645 getattr(self.builds[i], k), v,
2646 "Element %i in builds does not match" % (i,))
2647 except Exception:
2648 for build in self.builds:
2649 self.log.error("Running build: %s" % build)
2650 else:
2651 self.log.error("No running builds")
2652 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002653
James E. Blairb536ecc2016-08-31 10:11:42 -07002654 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002655 """Assert that the completed builds are as described.
2656
2657 The list of completed builds is examined and must match
2658 exactly the list of builds described by the input.
2659
2660 :arg list history: A list of dictionaries. Each item in the
2661 list must match the corresponding build in the build
2662 history, and each element of the dictionary must match the
2663 corresponding attribute of the build.
2664
James E. Blairb536ecc2016-08-31 10:11:42 -07002665 :arg bool ordered: If true, the history must match the order
2666 supplied, if false, the builds are permitted to have
2667 arrived in any order.
2668
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002669 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002670 def matches(history_item, item):
2671 for k, v in item.items():
2672 if getattr(history_item, k) != v:
2673 return False
2674 return True
James E. Blair3158e282016-08-19 09:34:11 -07002675 try:
2676 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002677 if ordered:
2678 for i, d in enumerate(history):
2679 if not matches(self.history[i], d):
2680 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002681 "Element %i in history does not match %s" %
2682 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002683 else:
2684 unseen = self.history[:]
2685 for i, d in enumerate(history):
2686 found = False
2687 for unseen_item in unseen:
2688 if matches(unseen_item, d):
2689 found = True
2690 unseen.remove(unseen_item)
2691 break
2692 if not found:
2693 raise Exception("No match found for element %i "
2694 "in history" % (i,))
2695 if unseen:
2696 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002697 except Exception:
2698 for build in self.history:
2699 self.log.error("Completed build: %s" % build)
2700 else:
2701 self.log.error("No completed builds")
2702 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002703
James E. Blair6ac368c2016-12-22 18:07:20 -08002704 def printHistory(self):
2705 """Log the build history.
2706
2707 This can be useful during tests to summarize what jobs have
2708 completed.
2709
2710 """
2711 self.log.debug("Build history:")
2712 for build in self.history:
2713 self.log.debug(build)
2714
James E. Blair59fdbac2015-12-07 17:08:06 -08002715 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002716 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2717
James E. Blair9ea70072017-04-19 16:05:30 -07002718 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002719 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002720 if not os.path.exists(root):
2721 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002722 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2723 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002724- tenant:
2725 name: openstack
2726 source:
2727 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002728 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002729 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002730 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002731 - org/project
2732 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002733 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002734 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002735 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002736 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002737 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002738
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002739 def addTagToRepo(self, project, name, sha):
2740 path = os.path.join(self.upstream_root, project)
2741 repo = git.Repo(path)
2742 repo.git.tag(name, sha)
2743
2744 def delTagFromRepo(self, project, name):
2745 path = os.path.join(self.upstream_root, project)
2746 repo = git.Repo(path)
2747 repo.git.tag('-d', name)
2748
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002749 def addCommitToRepo(self, project, message, files,
2750 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002751 path = os.path.join(self.upstream_root, project)
2752 repo = git.Repo(path)
2753 repo.head.reference = branch
2754 zuul.merger.merger.reset_repo_to_head(repo)
2755 for fn, content in files.items():
2756 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002757 try:
2758 os.makedirs(os.path.dirname(fn))
2759 except OSError:
2760 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002761 with open(fn, 'w') as f:
2762 f.write(content)
2763 repo.index.add([fn])
2764 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002765 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002766 repo.heads[branch].commit = commit
2767 repo.head.reference = branch
2768 repo.git.clean('-x', '-f', '-d')
2769 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002770 if tag:
2771 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002772 return before
2773
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002774 def commitConfigUpdate(self, project_name, source_name):
2775 """Commit an update to zuul.yaml
2776
2777 This overwrites the zuul.yaml in the specificed project with
2778 the contents specified.
2779
2780 :arg str project_name: The name of the project containing
2781 zuul.yaml (e.g., common-config)
2782
2783 :arg str source_name: The path to the file (underneath the
2784 test fixture directory) whose contents should be used to
2785 replace zuul.yaml.
2786 """
2787
2788 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002789 files = {}
2790 with open(source_path, 'r') as f:
2791 data = f.read()
2792 layout = yaml.safe_load(data)
2793 files['zuul.yaml'] = data
2794 for item in layout:
2795 if 'job' in item:
2796 jobname = item['job']['name']
2797 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002798 before = self.addCommitToRepo(
2799 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002800 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002801 return before
2802
Clint Byrum627ba362017-08-14 13:20:40 -07002803 def newTenantConfig(self, source_name):
2804 """ Use this to update the tenant config file in tests
2805
2806 This will update self.tenant_config_file to point to a temporary file
2807 for the duration of this particular test. The content of that file will
2808 be taken from FIXTURE_DIR/source_name
2809
2810 After the test the original value of self.tenant_config_file will be
2811 restored.
2812
2813 :arg str source_name: The path of the file under
2814 FIXTURE_DIR that will be used to populate the new tenant
2815 config file.
2816 """
2817 source_path = os.path.join(FIXTURE_DIR, source_name)
2818 orig_tenant_config_file = self.tenant_config_file
2819 with tempfile.NamedTemporaryFile(
2820 delete=False, mode='wb') as new_tenant_config:
2821 self.tenant_config_file = new_tenant_config.name
2822 with open(source_path, mode='rb') as source_tenant_config:
2823 new_tenant_config.write(source_tenant_config.read())
2824 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2825 self.setupAllProjectKeys()
2826 self.log.debug(
2827 'tenant_config_file = {}'.format(self.tenant_config_file))
2828
2829 def _restoreTenantConfig():
2830 self.log.debug(
2831 'restoring tenant_config_file = {}'.format(
2832 orig_tenant_config_file))
2833 os.unlink(self.tenant_config_file)
2834 self.tenant_config_file = orig_tenant_config_file
2835 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2836 self.addCleanup(_restoreTenantConfig)
2837
James E. Blair7fc8daa2016-08-08 15:37:15 -07002838 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002839
James E. Blair7fc8daa2016-08-08 15:37:15 -07002840 """Inject a Fake (Gerrit) event.
2841
2842 This method accepts a JSON-encoded event and simulates Zuul
2843 having received it from Gerrit. It could (and should)
2844 eventually apply to any connection type, but is currently only
2845 used with Gerrit connections. The name of the connection is
2846 used to look up the corresponding server, and the event is
2847 simulated as having been received by all Zuul connections
2848 attached to that server. So if two Gerrit connections in Zuul
2849 are connected to the same Gerrit server, and you invoke this
2850 method specifying the name of one of them, the event will be
2851 received by both.
2852
2853 .. note::
2854
2855 "self.fake_gerrit.addEvent" calls should be migrated to
2856 this method.
2857
2858 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002859 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002860 :arg str event: The JSON-encoded event.
2861
2862 """
2863 specified_conn = self.connections.connections[connection]
2864 for conn in self.connections.connections.values():
2865 if (isinstance(conn, specified_conn.__class__) and
2866 specified_conn.server == conn.server):
2867 conn.addEvent(event)
2868
James E. Blaird8af5422017-05-24 13:59:40 -07002869 def getUpstreamRepos(self, projects):
2870 """Return upstream git repo objects for the listed projects
2871
2872 :arg list projects: A list of strings, each the canonical name
2873 of a project.
2874
2875 :returns: A dictionary of {name: repo} for every listed
2876 project.
2877 :rtype: dict
2878
2879 """
2880
2881 repos = {}
2882 for project in projects:
2883 # FIXME(jeblair): the upstream root does not yet have a
2884 # hostname component; that needs to be added, and this
2885 # line removed:
2886 tmp_project_name = '/'.join(project.split('/')[1:])
2887 path = os.path.join(self.upstream_root, tmp_project_name)
2888 repo = git.Repo(path)
2889 repos[project] = repo
2890 return repos
2891
James E. Blair3f876d52016-07-22 13:07:14 -07002892
2893class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002894 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002895 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002896
Jamie Lennox7655b552017-03-17 12:33:38 +11002897 @contextmanager
2898 def jobLog(self, build):
2899 """Print job logs on assertion errors
2900
2901 This method is a context manager which, if it encounters an
2902 ecxeption, adds the build log to the debug output.
2903
2904 :arg Build build: The build that's being asserted.
2905 """
2906 try:
2907 yield
2908 except Exception:
2909 path = os.path.join(self.test_root, build.uuid,
2910 'work', 'logs', 'job-output.txt')
2911 with open(path) as f:
2912 self.log.debug(f.read())
2913 raise
2914
Joshua Heskethd78b4482015-09-14 16:56:34 -06002915
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002916class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002917 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002918 use_ssl = True
2919
2920
Joshua Heskethd78b4482015-09-14 16:56:34 -06002921class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002922 def setup_config(self):
2923 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002924 for section_name in self.config.sections():
2925 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2926 section_name, re.I)
2927 if not con_match:
2928 continue
2929
2930 if self.config.get(section_name, 'driver') == 'sql':
2931 f = MySQLSchemaFixture()
2932 self.useFixture(f)
2933 if (self.config.get(section_name, 'dburi') ==
2934 '$MYSQL_FIXTURE_DBURI$'):
2935 self.config.set(section_name, 'dburi', f.dburi)