blob: c13519ee96c3fc52c696eb8fce45bd78eb52e1d0 [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,
Tobias Henkel054eccc2017-12-20 11:36:19 +0100944 changes_db=None, upstream_root=None, git_url_with_auth=False):
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)
Tobias Henkel054eccc2017-12-20 11:36:19 +0100956 self.git_url_with_auth = git_url_with_auth
Jesse Keating80730e62017-09-14 15:35:11 -0600957 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200958
959 def getGithubClient(self,
960 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600961 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200962 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700963
Jesse Keating80730e62017-09-14 15:35:11 -0600964 def setZuulWebPort(self, port):
965 self.zuul_web_port = port
966
Jesse Keatinga41566f2017-06-14 18:17:51 -0700967 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700968 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700969 self.pr_number += 1
970 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100971 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700972 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800973 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700974 return pull_request
975
Jesse Keating71a47ff2017-06-06 11:36:43 -0700976 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
977 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700978 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -0700979 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -0700980 if not new_rev:
981 new_rev = random_sha1()
982 name = 'push'
983 data = {
984 'ref': ref,
985 'before': old_rev,
986 'after': new_rev,
987 'repository': {
988 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -0700989 },
990 'commits': [
991 {
992 'added': added_files,
993 'removed': removed_files,
994 'modified': modified_files
995 }
996 ]
Wayne1a78c612015-06-11 17:14:13 -0700997 }
998 return (name, data)
999
Jesse Keating80730e62017-09-14 15:35:11 -06001000 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001001 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001002 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001003 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001004 secret = self.connection_config['webhook_token']
1005 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001006 headers = {'x-github-event': name, 'x-hub-signature': signature}
1007
1008 if use_zuulweb:
1009 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001010 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001011 % (self.zuul_web_port, self.connection_name),
1012 data=payload, headers=headers)
1013 return urllib.request.urlopen(req)
1014 else:
1015 job = self.rpcclient.submitJob(
1016 'github:%s:payload' % self.connection_name,
1017 {'headers': headers, 'body': data})
1018 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001019
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001020 def addProject(self, project):
1021 # use the original method here and additionally register it in the
1022 # fake github
1023 super(FakeGithubConnection, self).addProject(project)
1024 self.getGithubClient(project).addProject(project)
1025
Jesse Keating9021a012017-08-29 14:45:27 -07001026 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001027 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001028 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001029 if len(prs) > 1:
1030 raise Exception('Multiple pulls found with head sha: %s' % sha)
1031 pr = prs[0]
1032 return self.getPull(pr.project, pr.number)
1033
Jesse Keatingae4cd272017-01-30 17:10:44 -08001034 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001035 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001036 return pr.reviews
1037
Jesse Keatingae4cd272017-01-30 17:10:44 -08001038 def getRepoPermission(self, project, login):
1039 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001040 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001041 pr_owner, pr_project = pr.project.split('/')
1042 if (pr_owner == owner and proj == pr_project):
1043 if login in pr.writers:
1044 return 'write'
1045 else:
1046 return 'read'
1047
Gregory Haynes4fc12542015-04-22 20:38:06 -07001048 def getGitUrl(self, project):
Tobias Henkel054eccc2017-12-20 11:36:19 +01001049 if self.git_url_with_auth:
1050 auth_token = ''.join(
1051 random.choice(string.ascii_lowercase) for x in range(8))
1052 prefix = 'file://x-access-token:%s@' % auth_token
1053 else:
1054 prefix = ''
1055 return prefix + os.path.join(self.upstream_root, str(project))
Gregory Haynes4fc12542015-04-22 20:38:06 -07001056
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001057 def real_getGitUrl(self, project):
1058 return super(FakeGithubConnection, self).getGitUrl(project)
1059
Jan Hrubane252a732017-01-03 15:03:09 +01001060 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001061 # record that this got reported
1062 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001063 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001064 pull_request.addComment(message)
1065
Jan Hruban3b415922016-02-03 13:10:22 +01001066 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001067 # record that this got reported
1068 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001069 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001070 if self.merge_failure:
1071 raise Exception('Pull request was not merged')
1072 if self.merge_not_allowed_count > 0:
1073 self.merge_not_allowed_count -= 1
1074 raise MergeFailure('Merge was not successful due to mergeability'
1075 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001076 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001077
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001078 def setCommitStatus(self, project, sha, state, url='', description='',
1079 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001080 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001081 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001082 super(FakeGithubConnection, self).setCommitStatus(
1083 project, sha, state,
1084 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001085
Jan Hruban16ad31f2015-11-07 14:39:07 +01001086 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001087 # record that this got reported
1088 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001089 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001090 pull_request.addLabel(label)
1091
1092 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001093 # record that this got reported
1094 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001095 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001096 pull_request.removeLabel(label)
1097
Gregory Haynes4fc12542015-04-22 20:38:06 -07001098
Clark Boylanb640e052014-04-03 16:41:46 -07001099class BuildHistory(object):
1100 def __init__(self, **kw):
1101 self.__dict__.update(kw)
1102
1103 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001104 return ("<Completed build, result: %s name: %s uuid: %s "
1105 "changes: %s ref: %s>" %
1106 (self.result, self.name, self.uuid,
1107 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001108
1109
Clark Boylanb640e052014-04-03 16:41:46 -07001110class FakeStatsd(threading.Thread):
1111 def __init__(self):
1112 threading.Thread.__init__(self)
1113 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001114 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001115 self.sock.bind(('', 0))
1116 self.port = self.sock.getsockname()[1]
1117 self.wake_read, self.wake_write = os.pipe()
1118 self.stats = []
1119
1120 def run(self):
1121 while True:
1122 poll = select.poll()
1123 poll.register(self.sock, select.POLLIN)
1124 poll.register(self.wake_read, select.POLLIN)
1125 ret = poll.poll()
1126 for (fd, event) in ret:
1127 if fd == self.sock.fileno():
1128 data = self.sock.recvfrom(1024)
1129 if not data:
1130 return
1131 self.stats.append(data[0])
1132 if fd == self.wake_read:
1133 return
1134
1135 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001136 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001137
1138
James E. Blaire1767bc2016-08-02 10:00:27 -07001139class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001140 log = logging.getLogger("zuul.test")
1141
Paul Belanger174a8272017-03-14 13:20:10 -04001142 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001143 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001144 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001145 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001146 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001147 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001148 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001149 # TODOv3(jeblair): self.node is really "the label of the node
1150 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001151 # keep using it like this, or we may end up exposing more of
1152 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001153 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001154 self.node = None
1155 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001156 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001157 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001158 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001159 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001160 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001161 self.wait_condition = threading.Condition()
1162 self.waiting = False
1163 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001164 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001165 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001166 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001167 items = self.parameters['zuul']['items']
1168 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1169 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001170
James E. Blair3158e282016-08-19 09:34:11 -07001171 def __repr__(self):
1172 waiting = ''
1173 if self.waiting:
1174 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001175 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1176 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001177
Clark Boylanb640e052014-04-03 16:41:46 -07001178 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001179 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001180 self.wait_condition.acquire()
1181 self.wait_condition.notify()
1182 self.waiting = False
1183 self.log.debug("Build %s released" % self.unique)
1184 self.wait_condition.release()
1185
1186 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001187 """Return whether this build is being held.
1188
1189 :returns: Whether the build is being held.
1190 :rtype: bool
1191 """
1192
Clark Boylanb640e052014-04-03 16:41:46 -07001193 self.wait_condition.acquire()
1194 if self.waiting:
1195 ret = True
1196 else:
1197 ret = False
1198 self.wait_condition.release()
1199 return ret
1200
1201 def _wait(self):
1202 self.wait_condition.acquire()
1203 self.waiting = True
1204 self.log.debug("Build %s waiting" % self.unique)
1205 self.wait_condition.wait()
1206 self.wait_condition.release()
1207
1208 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001209 self.log.debug('Running build %s' % self.unique)
1210
Paul Belanger174a8272017-03-14 13:20:10 -04001211 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001212 self.log.debug('Holding build %s' % self.unique)
1213 self._wait()
1214 self.log.debug("Build %s continuing" % self.unique)
1215
James E. Blair412fba82017-01-26 15:00:50 -08001216 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001217 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001218 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001219 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001220 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001221 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001222 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001223
James E. Blaire1767bc2016-08-02 10:00:27 -07001224 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001225
James E. Blaira5dba232016-08-08 15:53:24 -07001226 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001227 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001228 for change in changes:
1229 if self.hasChanges(change):
1230 return True
1231 return False
1232
James E. Blaire7b99a02016-08-05 14:27:34 -07001233 def hasChanges(self, *changes):
1234 """Return whether this build has certain changes in its git repos.
1235
1236 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001237 are expected to be present (in order) in the git repository of
1238 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001239
1240 :returns: Whether the build has the indicated changes.
1241 :rtype: bool
1242
1243 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001244 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001245 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001246 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001247 try:
1248 repo = git.Repo(path)
1249 except NoSuchPathError as e:
1250 self.log.debug('%s' % e)
1251 return False
James E. Blair247cab72017-07-20 16:52:36 -07001252 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001253 commit_message = '%s-1' % change.subject
1254 self.log.debug("Checking if build %s has changes; commit_message "
1255 "%s; repo_messages %s" % (self, commit_message,
1256 repo_messages))
1257 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001258 self.log.debug(" messages do not match")
1259 return False
1260 self.log.debug(" OK")
1261 return True
1262
James E. Blaird8af5422017-05-24 13:59:40 -07001263 def getWorkspaceRepos(self, projects):
1264 """Return workspace git repo objects for the listed projects
1265
1266 :arg list projects: A list of strings, each the canonical name
1267 of a project.
1268
1269 :returns: A dictionary of {name: repo} for every listed
1270 project.
1271 :rtype: dict
1272
1273 """
1274
1275 repos = {}
1276 for project in projects:
1277 path = os.path.join(self.jobdir.src_root, project)
1278 repo = git.Repo(path)
1279 repos[project] = repo
1280 return repos
1281
Clark Boylanb640e052014-04-03 16:41:46 -07001282
James E. Blair107bb252017-10-13 15:53:16 -07001283class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1284 def doMergeChanges(self, merger, items, repo_state):
1285 # Get a merger in order to update the repos involved in this job.
1286 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1287 merger, items, repo_state)
1288 if not commit: # merge conflict
1289 self.recordResult('MERGER_FAILURE')
1290 return commit
1291
1292 def recordResult(self, result):
1293 build = self.executor_server.job_builds[self.job.unique]
1294 self.executor_server.lock.acquire()
1295 self.executor_server.build_history.append(
1296 BuildHistory(name=build.name, result=result, changes=build.changes,
1297 node=build.node, uuid=build.unique,
1298 ref=build.parameters['zuul']['ref'],
1299 parameters=build.parameters, jobdir=build.jobdir,
1300 pipeline=build.parameters['zuul']['pipeline'])
1301 )
1302 self.executor_server.running_builds.remove(build)
1303 del self.executor_server.job_builds[self.job.unique]
1304 self.executor_server.lock.release()
1305
1306 def runPlaybooks(self, args):
1307 build = self.executor_server.job_builds[self.job.unique]
1308 build.jobdir = self.jobdir
1309
1310 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1311 self.recordResult(result)
1312 return result
1313
James E. Blaira86aaf12017-10-15 20:59:50 -07001314 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001315 build = self.executor_server.job_builds[self.job.unique]
1316
1317 if self.executor_server._run_ansible:
1318 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001319 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001320 else:
1321 if playbook.path:
1322 result = build.run()
1323 else:
1324 result = (self.RESULT_NORMAL, 0)
1325 return result
1326
1327 def getHostList(self, args):
1328 self.log.debug("hostlist")
1329 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1330 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001331 if not host['host_vars'].get('ansible_connection'):
1332 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001333
1334 hosts.append(dict(
Paul Belangerecb0b842017-11-18 15:23:29 -05001335 name=['localhost'],
James E. Blair107bb252017-10-13 15:53:16 -07001336 host_vars=dict(ansible_connection='local'),
1337 host_keys=[]))
1338 return hosts
1339
1340
Paul Belanger174a8272017-03-14 13:20:10 -04001341class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1342 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001343
Paul Belanger174a8272017-03-14 13:20:10 -04001344 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001345 they will report that they have started but then pause until
1346 released before reporting completion. This attribute may be
1347 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001348 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001349 be explicitly released.
1350
1351 """
James E. Blairfaf81982017-10-10 15:42:26 -07001352
1353 _job_class = RecordingAnsibleJob
1354
James E. Blairf5dbd002015-12-23 15:26:17 -08001355 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001356 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001357 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001358 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001359 self.hold_jobs_in_build = False
1360 self.lock = threading.Lock()
1361 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001362 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001363 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001364 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001365
James E. Blaira5dba232016-08-08 15:53:24 -07001366 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001367 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001368
1369 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001370 :arg Change change: The :py:class:`~tests.base.FakeChange`
1371 instance which should cause the job to fail. This job
1372 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001373
1374 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001375 l = self.fail_tests.get(name, [])
1376 l.append(change)
1377 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001378
James E. Blair962220f2016-08-03 11:22:38 -07001379 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001380 """Release a held build.
1381
1382 :arg str regex: A regular expression which, if supplied, will
1383 cause only builds with matching names to be released. If
1384 not supplied, all builds will be released.
1385
1386 """
James E. Blair962220f2016-08-03 11:22:38 -07001387 builds = self.running_builds[:]
1388 self.log.debug("Releasing build %s (%s)" % (regex,
1389 len(self.running_builds)))
1390 for build in builds:
1391 if not regex or re.match(regex, build.name):
1392 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001393 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001394 build.release()
1395 else:
1396 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001397 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001398 self.log.debug("Done releasing builds %s (%s)" %
1399 (regex, len(self.running_builds)))
1400
Paul Belanger174a8272017-03-14 13:20:10 -04001401 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001402 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001403 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001404 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001405 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001406 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001407 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001408 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001409 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001410
1411 def stopJob(self, job):
1412 self.log.debug("handle stop")
1413 parameters = json.loads(job.arguments)
1414 uuid = parameters['uuid']
1415 for build in self.running_builds:
1416 if build.unique == uuid:
1417 build.aborted = True
1418 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001419 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001420
James E. Blaira002b032017-04-18 10:35:48 -07001421 def stop(self):
1422 for build in self.running_builds:
1423 build.release()
1424 super(RecordingExecutorServer, self).stop()
1425
Joshua Hesketh50c21782016-10-13 21:34:14 +11001426
Clark Boylanb640e052014-04-03 16:41:46 -07001427class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001428 """A Gearman server for use in tests.
1429
1430 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1431 added to the queue but will not be distributed to workers
1432 until released. This attribute may be changed at any time and
1433 will take effect for subsequently enqueued jobs, but
1434 previously held jobs will still need to be explicitly
1435 released.
1436
1437 """
1438
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001439 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001440 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001441 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001442 if use_ssl:
1443 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1444 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1445 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1446 else:
1447 ssl_ca = None
1448 ssl_cert = None
1449 ssl_key = None
1450
1451 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1452 ssl_cert=ssl_cert,
1453 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001454
1455 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001456 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1457 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001458 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001459 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001460 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001461 elif job.name.startswith(b'merger:'):
1462 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001463 else:
1464 job.waiting = False
1465 if job.waiting:
1466 continue
1467 if job.name in connection.functions:
1468 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001469 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001470 connection.related_jobs[job.handle] = job
1471 job.worker_connection = connection
1472 job.running = True
1473 return job
1474 return None
1475
1476 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001477 """Release a held job.
1478
1479 :arg str regex: A regular expression which, if supplied, will
1480 cause only jobs with matching names to be released. If
1481 not supplied, all jobs will be released.
1482 """
Clark Boylanb640e052014-04-03 16:41:46 -07001483 released = False
1484 qlen = (len(self.high_queue) + len(self.normal_queue) +
1485 len(self.low_queue))
1486 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1487 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001488 match = False
1489 if job.name == b'executor:execute':
1490 parameters = json.loads(job.arguments.decode('utf8'))
1491 if not regex or re.match(regex, parameters.get('job')):
1492 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001493 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001494 if not regex:
1495 match = True
1496 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001497 self.log.debug("releasing queued job %s" %
1498 job.unique)
1499 job.waiting = False
1500 released = True
1501 else:
1502 self.log.debug("not releasing queued job %s" %
1503 job.unique)
1504 if released:
1505 self.wakeConnections()
1506 qlen = (len(self.high_queue) + len(self.normal_queue) +
1507 len(self.low_queue))
1508 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1509
1510
1511class FakeSMTP(object):
1512 log = logging.getLogger('zuul.FakeSMTP')
1513
1514 def __init__(self, messages, server, port):
1515 self.server = server
1516 self.port = port
1517 self.messages = messages
1518
1519 def sendmail(self, from_email, to_email, msg):
1520 self.log.info("Sending email from %s, to %s, with msg %s" % (
1521 from_email, to_email, msg))
1522
1523 headers = msg.split('\n\n', 1)[0]
1524 body = msg.split('\n\n', 1)[1]
1525
1526 self.messages.append(dict(
1527 from_email=from_email,
1528 to_email=to_email,
1529 msg=msg,
1530 headers=headers,
1531 body=body,
1532 ))
1533
1534 return True
1535
1536 def quit(self):
1537 return True
1538
1539
James E. Blairdce6cea2016-12-20 16:45:32 -08001540class FakeNodepool(object):
1541 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001542 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001543
1544 log = logging.getLogger("zuul.test.FakeNodepool")
1545
1546 def __init__(self, host, port, chroot):
1547 self.client = kazoo.client.KazooClient(
1548 hosts='%s:%s%s' % (host, port, chroot))
1549 self.client.start()
1550 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001551 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001552 self.thread = threading.Thread(target=self.run)
1553 self.thread.daemon = True
1554 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001555 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001556
1557 def stop(self):
1558 self._running = False
1559 self.thread.join()
1560 self.client.stop()
1561 self.client.close()
1562
1563 def run(self):
1564 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001565 try:
1566 self._run()
1567 except Exception:
1568 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001569 time.sleep(0.1)
1570
1571 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001572 if self.paused:
1573 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001574 for req in self.getNodeRequests():
1575 self.fulfillRequest(req)
1576
1577 def getNodeRequests(self):
1578 try:
1579 reqids = self.client.get_children(self.REQUEST_ROOT)
1580 except kazoo.exceptions.NoNodeError:
1581 return []
1582 reqs = []
1583 for oid in sorted(reqids):
1584 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001585 try:
1586 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001587 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001588 data['_oid'] = oid
1589 reqs.append(data)
1590 except kazoo.exceptions.NoNodeError:
1591 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001592 return reqs
1593
James E. Blaire18d4602017-01-05 11:17:28 -08001594 def getNodes(self):
1595 try:
1596 nodeids = self.client.get_children(self.NODE_ROOT)
1597 except kazoo.exceptions.NoNodeError:
1598 return []
1599 nodes = []
1600 for oid in sorted(nodeids):
1601 path = self.NODE_ROOT + '/' + oid
1602 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001603 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001604 data['_oid'] = oid
1605 try:
1606 lockfiles = self.client.get_children(path + '/lock')
1607 except kazoo.exceptions.NoNodeError:
1608 lockfiles = []
1609 if lockfiles:
1610 data['_lock'] = True
1611 else:
1612 data['_lock'] = False
1613 nodes.append(data)
1614 return nodes
1615
James E. Blaira38c28e2017-01-04 10:33:20 -08001616 def makeNode(self, request_id, node_type):
1617 now = time.time()
1618 path = '/nodepool/nodes/'
1619 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001620 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001621 provider='test-provider',
1622 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001623 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001624 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001625 public_ipv4='127.0.0.1',
1626 private_ipv4=None,
1627 public_ipv6=None,
1628 allocated_to=request_id,
1629 state='ready',
1630 state_time=now,
1631 created_time=now,
1632 updated_time=now,
1633 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001634 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001635 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001636 if 'fakeuser' in node_type:
1637 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001638 if 'windows' in node_type:
1639 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001640 if 'network' in node_type:
1641 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001642
Clint Byrumf322fe22017-05-10 20:53:12 -07001643 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001644 path = self.client.create(path, data,
1645 makepath=True,
1646 sequence=True)
1647 nodeid = path.split("/")[-1]
1648 return nodeid
1649
James E. Blair6ab79e02017-01-06 10:10:17 -08001650 def addFailRequest(self, request):
1651 self.fail_requests.add(request['_oid'])
1652
James E. Blairdce6cea2016-12-20 16:45:32 -08001653 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001654 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001655 return
1656 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001657 oid = request['_oid']
1658 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001659
James E. Blair6ab79e02017-01-06 10:10:17 -08001660 if oid in self.fail_requests:
1661 request['state'] = 'failed'
1662 else:
1663 request['state'] = 'fulfilled'
1664 nodes = []
1665 for node in request['node_types']:
1666 nodeid = self.makeNode(oid, node)
1667 nodes.append(nodeid)
1668 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001669
James E. Blaira38c28e2017-01-04 10:33:20 -08001670 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001671 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001672 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001673 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001674 try:
1675 self.client.set(path, data)
1676 except kazoo.exceptions.NoNodeError:
1677 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001678
1679
James E. Blair498059b2016-12-20 13:50:13 -08001680class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001681 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001682 super(ChrootedKazooFixture, self).__init__()
1683
1684 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1685 if ':' in zk_host:
1686 host, port = zk_host.split(':')
1687 else:
1688 host = zk_host
1689 port = None
1690
1691 self.zookeeper_host = host
1692
1693 if not port:
1694 self.zookeeper_port = 2181
1695 else:
1696 self.zookeeper_port = int(port)
1697
Clark Boylan621ec9a2017-04-07 17:41:33 -07001698 self.test_id = test_id
1699
James E. Blair498059b2016-12-20 13:50:13 -08001700 def _setUp(self):
1701 # Make sure the test chroot paths do not conflict
1702 random_bits = ''.join(random.choice(string.ascii_lowercase +
1703 string.ascii_uppercase)
1704 for x in range(8))
1705
Clark Boylan621ec9a2017-04-07 17:41:33 -07001706 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001707 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1708
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001709 self.addCleanup(self._cleanup)
1710
James E. Blair498059b2016-12-20 13:50:13 -08001711 # Ensure the chroot path exists and clean up any pre-existing znodes.
1712 _tmp_client = kazoo.client.KazooClient(
1713 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1714 _tmp_client.start()
1715
1716 if _tmp_client.exists(self.zookeeper_chroot):
1717 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1718
1719 _tmp_client.ensure_path(self.zookeeper_chroot)
1720 _tmp_client.stop()
1721 _tmp_client.close()
1722
James E. Blair498059b2016-12-20 13:50:13 -08001723 def _cleanup(self):
1724 '''Remove the chroot path.'''
1725 # Need a non-chroot'ed client to remove the chroot path
1726 _tmp_client = kazoo.client.KazooClient(
1727 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1728 _tmp_client.start()
1729 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1730 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001731 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001732
1733
Joshua Heskethd78b4482015-09-14 16:56:34 -06001734class MySQLSchemaFixture(fixtures.Fixture):
1735 def setUp(self):
1736 super(MySQLSchemaFixture, self).setUp()
1737
1738 random_bits = ''.join(random.choice(string.ascii_lowercase +
1739 string.ascii_uppercase)
1740 for x in range(8))
1741 self.name = '%s_%s' % (random_bits, os.getpid())
1742 self.passwd = uuid.uuid4().hex
1743 db = pymysql.connect(host="localhost",
1744 user="openstack_citest",
1745 passwd="openstack_citest",
1746 db="openstack_citest")
1747 cur = db.cursor()
1748 cur.execute("create database %s" % self.name)
1749 cur.execute(
1750 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1751 (self.name, self.name, self.passwd))
1752 cur.execute("flush privileges")
1753
1754 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1755 self.passwd,
1756 self.name)
1757 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1758 self.addCleanup(self.cleanup)
1759
1760 def cleanup(self):
1761 db = pymysql.connect(host="localhost",
1762 user="openstack_citest",
1763 passwd="openstack_citest",
1764 db="openstack_citest")
1765 cur = db.cursor()
1766 cur.execute("drop database %s" % self.name)
1767 cur.execute("drop user '%s'@'localhost'" % self.name)
1768 cur.execute("flush privileges")
1769
1770
Maru Newby3fe5f852015-01-13 04:22:14 +00001771class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001772 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001773 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001774
James E. Blair1c236df2017-02-01 14:07:24 -08001775 def attachLogs(self, *args):
1776 def reader():
1777 self._log_stream.seek(0)
1778 while True:
1779 x = self._log_stream.read(4096)
1780 if not x:
1781 break
1782 yield x.encode('utf8')
1783 content = testtools.content.content_from_reader(
1784 reader,
1785 testtools.content_type.UTF8_TEXT,
1786 False)
1787 self.addDetail('logging', content)
1788
Clark Boylanb640e052014-04-03 16:41:46 -07001789 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001790 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001791 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1792 try:
1793 test_timeout = int(test_timeout)
1794 except ValueError:
1795 # If timeout value is invalid do not set a timeout.
1796 test_timeout = 0
1797 if test_timeout > 0:
1798 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1799
1800 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1801 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1802 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1803 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1804 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1805 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1806 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1807 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1808 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1809 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001810 self._log_stream = StringIO()
1811 self.addOnException(self.attachLogs)
1812 else:
1813 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001814
James E. Blair1c236df2017-02-01 14:07:24 -08001815 handler = logging.StreamHandler(self._log_stream)
1816 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1817 '%(levelname)-8s %(message)s')
1818 handler.setFormatter(formatter)
1819
1820 logger = logging.getLogger()
1821 logger.setLevel(logging.DEBUG)
1822 logger.addHandler(handler)
1823
Clark Boylan3410d532017-04-25 12:35:29 -07001824 # Make sure we don't carry old handlers around in process state
1825 # which slows down test runs
1826 self.addCleanup(logger.removeHandler, handler)
1827 self.addCleanup(handler.close)
1828 self.addCleanup(handler.flush)
1829
James E. Blair1c236df2017-02-01 14:07:24 -08001830 # NOTE(notmorgan): Extract logging overrides for specific
1831 # libraries from the OS_LOG_DEFAULTS env and create loggers
1832 # for each. This is used to limit the output during test runs
1833 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001834 log_defaults_from_env = os.environ.get(
1835 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001836 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001837
James E. Blairdce6cea2016-12-20 16:45:32 -08001838 if log_defaults_from_env:
1839 for default in log_defaults_from_env.split(','):
1840 try:
1841 name, level_str = default.split('=', 1)
1842 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001843 logger = logging.getLogger(name)
1844 logger.setLevel(level)
1845 logger.addHandler(handler)
1846 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001847 except ValueError:
1848 # NOTE(notmorgan): Invalid format of the log default,
1849 # skip and don't try and apply a logger for the
1850 # specified module
1851 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001852
Maru Newby3fe5f852015-01-13 04:22:14 +00001853
1854class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001855 """A test case with a functioning Zuul.
1856
1857 The following class variables are used during test setup and can
1858 be overidden by subclasses but are effectively read-only once a
1859 test method starts running:
1860
1861 :cvar str config_file: This points to the main zuul config file
1862 within the fixtures directory. Subclasses may override this
1863 to obtain a different behavior.
1864
1865 :cvar str tenant_config_file: This is the tenant config file
1866 (which specifies from what git repos the configuration should
1867 be loaded). It defaults to the value specified in
1868 `config_file` but can be overidden by subclasses to obtain a
1869 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001870 configuration. See also the :py:func:`simple_layout`
1871 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001872
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001873 :cvar bool create_project_keys: Indicates whether Zuul should
1874 auto-generate keys for each project, or whether the test
1875 infrastructure should insert dummy keys to save time during
1876 startup. Defaults to False.
1877
James E. Blaire7b99a02016-08-05 14:27:34 -07001878 The following are instance variables that are useful within test
1879 methods:
1880
1881 :ivar FakeGerritConnection fake_<connection>:
1882 A :py:class:`~tests.base.FakeGerritConnection` will be
1883 instantiated for each connection present in the config file
1884 and stored here. For instance, `fake_gerrit` will hold the
1885 FakeGerritConnection object for a connection named `gerrit`.
1886
1887 :ivar FakeGearmanServer gearman_server: An instance of
1888 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1889 server that all of the Zuul components in this test use to
1890 communicate with each other.
1891
Paul Belanger174a8272017-03-14 13:20:10 -04001892 :ivar RecordingExecutorServer executor_server: An instance of
1893 :py:class:`~tests.base.RecordingExecutorServer` which is the
1894 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001895
1896 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1897 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001898 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001899 list upon completion.
1900
1901 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1902 objects representing completed builds. They are appended to
1903 the list in the order they complete.
1904
1905 """
1906
James E. Blair83005782015-12-11 14:46:03 -08001907 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001908 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001909 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001910 use_ssl = False
Tobias Henkel054eccc2017-12-20 11:36:19 +01001911 git_url_with_auth = False
James E. Blair3f876d52016-07-22 13:07:14 -07001912
1913 def _startMerger(self):
1914 self.merge_server = zuul.merger.server.MergeServer(self.config,
1915 self.connections)
1916 self.merge_server.start()
1917
Maru Newby3fe5f852015-01-13 04:22:14 +00001918 def setUp(self):
1919 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08001920
1921 self.setupZK()
1922
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001923 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07001924 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10001925 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
1926 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07001927 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08001928 tmp_root = tempfile.mkdtemp(
1929 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07001930 self.test_root = os.path.join(tmp_root, "zuul-test")
1931 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05001932 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04001933 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07001934 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01001935 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
1936 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07001937
1938 if os.path.exists(self.test_root):
1939 shutil.rmtree(self.test_root)
1940 os.makedirs(self.test_root)
1941 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07001942 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01001943 os.makedirs(self.merger_state_root)
1944 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07001945
1946 # Make per test copy of Configuration.
1947 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07001948 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
1949 if not os.path.exists(self.private_key_file):
1950 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
1951 shutil.copy(src_private_key_file, self.private_key_file)
1952 shutil.copy('{}.pub'.format(src_private_key_file),
1953 '{}.pub'.format(self.private_key_file))
1954 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01001955 self.config.set('scheduler', 'tenant_config',
1956 os.path.join(
1957 FIXTURE_DIR,
1958 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01001959 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05001960 self.config.set(
1961 'scheduler', 'command_socket',
1962 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05001963 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04001964 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07001965 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01001966 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05001967 self.config.set(
1968 'executor', 'command_socket',
1969 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08001970 self.config.set(
1971 'merger', 'command_socket',
1972 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07001973
Clark Boylanb640e052014-04-03 16:41:46 -07001974 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07001975 if self.config.has_section('statsd'):
1976 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07001977 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07001978
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001979 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07001980
1981 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08001982 self.log.info("Gearman server on port %s" %
1983 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001984 if self.use_ssl:
1985 self.log.info('SSL enabled for gearman')
1986 self.config.set(
1987 'gearman', 'ssl_ca',
1988 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
1989 self.config.set(
1990 'gearman', 'ssl_cert',
1991 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
1992 self.config.set(
1993 'gearman', 'ssl_key',
1994 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07001995
Jesse Keating80730e62017-09-14 15:35:11 -06001996 self.rpcclient = zuul.rpcclient.RPCClient(
1997 self.config.get('gearman', 'server'),
1998 self.gearman_server.port,
1999 get_default(self.config, 'gearman', 'ssl_key'),
2000 get_default(self.config, 'gearman', 'ssl_cert'),
2001 get_default(self.config, 'gearman', 'ssl_ca'))
2002
James E. Blaire511d2f2016-12-08 15:22:26 -08002003 gerritsource.GerritSource.replication_timeout = 1.5
2004 gerritsource.GerritSource.replication_retry_interval = 0.5
2005 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002006
Joshua Hesketh352264b2015-08-11 23:42:08 +10002007 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002008 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002009
Jan Hruban6b71aff2015-10-22 16:58:08 +02002010 self.event_queues = [
2011 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002012 self.sched.trigger_event_queue,
2013 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002014 ]
2015
James E. Blairfef78942016-03-11 16:28:56 -08002016 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002017 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002018
Paul Belanger174a8272017-03-14 13:20:10 -04002019 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002020 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002021 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002022 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002023 _test_root=self.test_root,
2024 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002025 self.executor_server.start()
2026 self.history = self.executor_server.build_history
2027 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002028
Paul Belanger174a8272017-03-14 13:20:10 -04002029 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002030 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002031 self.merge_client = zuul.merger.client.MergeClient(
2032 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002033 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002034 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002035 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002036 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002037
James E. Blair0d5a36e2017-02-21 10:53:44 -05002038 self.fake_nodepool = FakeNodepool(
2039 self.zk_chroot_fixture.zookeeper_host,
2040 self.zk_chroot_fixture.zookeeper_port,
2041 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002042
Paul Belanger174a8272017-03-14 13:20:10 -04002043 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002044 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002045 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002046 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002047
Clark Boylanb640e052014-04-03 16:41:46 -07002048 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002049 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002050 # Cleanups are run in reverse order
2051 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002052 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002053 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002054
James E. Blairb9c0d772017-03-03 14:34:49 -08002055 self.sched.reconfigure(self.config)
2056 self.sched.resume()
2057
Tobias Henkel7df274b2017-05-26 17:41:11 +02002058 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002059 # Set up gerrit related fakes
2060 # Set a changes database so multiple FakeGerrit's can report back to
2061 # a virtual canonical database given by the configured hostname
2062 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002063 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002064
2065 def getGerritConnection(driver, name, config):
2066 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2067 con = FakeGerritConnection(driver, name, config,
2068 changes_db=db,
2069 upstream_root=self.upstream_root)
2070 self.event_queues.append(con.event_queue)
2071 setattr(self, 'fake_' + name, con)
2072 return con
2073
2074 self.useFixture(fixtures.MonkeyPatch(
2075 'zuul.driver.gerrit.GerritDriver.getConnection',
2076 getGerritConnection))
2077
Gregory Haynes4fc12542015-04-22 20:38:06 -07002078 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002079 server = config.get('server', 'github.com')
2080 db = self.github_changes_dbs.setdefault(server, {})
Tobias Henkel054eccc2017-12-20 11:36:19 +01002081 con = FakeGithubConnection(
2082 driver, name, config,
2083 self.rpcclient,
2084 changes_db=db,
2085 upstream_root=self.upstream_root,
2086 git_url_with_auth=self.git_url_with_auth)
Jesse Keating64d29012017-09-06 12:27:49 -07002087 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002088 setattr(self, 'fake_' + name, con)
2089 return con
2090
2091 self.useFixture(fixtures.MonkeyPatch(
2092 'zuul.driver.github.GithubDriver.getConnection',
2093 getGithubConnection))
2094
James E. Blaire511d2f2016-12-08 15:22:26 -08002095 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002096 # TODO(jhesketh): This should come from lib.connections for better
2097 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002098 # Register connections from the config
2099 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002100
Joshua Hesketh352264b2015-08-11 23:42:08 +10002101 def FakeSMTPFactory(*args, **kw):
2102 args = [self.smtp_messages] + list(args)
2103 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002104
Joshua Hesketh352264b2015-08-11 23:42:08 +10002105 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002106
James E. Blaire511d2f2016-12-08 15:22:26 -08002107 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002108 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002109 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002110
James E. Blair83005782015-12-11 14:46:03 -08002111 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002112 # This creates the per-test configuration object. It can be
2113 # overriden by subclasses, but should not need to be since it
2114 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002115 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002116 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002117
James E. Blair39840362017-06-23 20:34:02 +01002118 sections = ['zuul', 'scheduler', 'executor', 'merger']
2119 for section in sections:
2120 if not self.config.has_section(section):
2121 self.config.add_section(section)
2122
James E. Blair06cc3922017-04-19 10:08:10 -07002123 if not self.setupSimpleLayout():
2124 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002125 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002126 self.tenant_config_file)
2127 git_path = os.path.join(
2128 os.path.dirname(
2129 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2130 'git')
2131 if os.path.exists(git_path):
2132 for reponame in os.listdir(git_path):
2133 project = reponame.replace('_', '/')
2134 self.copyDirToRepo(project,
2135 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002136 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002137 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002138 self.setupAllProjectKeys()
2139
James E. Blair06cc3922017-04-19 10:08:10 -07002140 def setupSimpleLayout(self):
2141 # If the test method has been decorated with a simple_layout,
2142 # use that instead of the class tenant_config_file. Set up a
2143 # single config-project with the specified layout, and
2144 # initialize repos for all of the 'project' entries which
2145 # appear in the layout.
2146 test_name = self.id().split('.')[-1]
2147 test = getattr(self, test_name)
2148 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002149 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002150 else:
2151 return False
2152
James E. Blairb70e55a2017-04-19 12:57:02 -07002153 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002154 path = os.path.join(FIXTURE_DIR, path)
2155 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002156 data = f.read()
2157 layout = yaml.safe_load(data)
2158 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002159 untrusted_projects = []
2160 for item in layout:
2161 if 'project' in item:
2162 name = item['project']['name']
2163 untrusted_projects.append(name)
2164 self.init_repo(name)
2165 self.addCommitToRepo(name, 'initial commit',
2166 files={'README': ''},
2167 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002168 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002169 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002170 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002171 for fn in zuul.configloader.as_list(
2172 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002173 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002174 for fn in zuul.configloader.as_list(
2175 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002176 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002177
2178 root = os.path.join(self.test_root, "config")
2179 if not os.path.exists(root):
2180 os.makedirs(root)
2181 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2182 config = [{'tenant':
2183 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002184 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002185 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002186 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002187 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002188 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002189 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002190 os.path.join(FIXTURE_DIR, f.name))
2191
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002192 self.init_repo('org/common-config')
2193 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002194 files, branch='master', tag='init')
2195
2196 return True
2197
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002198 def setupAllProjectKeys(self):
2199 if self.create_project_keys:
2200 return
2201
James E. Blair39840362017-06-23 20:34:02 +01002202 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002203 with open(os.path.join(FIXTURE_DIR, path)) as f:
2204 tenant_config = yaml.safe_load(f.read())
2205 for tenant in tenant_config:
2206 sources = tenant['tenant']['source']
2207 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002208 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002209 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002210 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002211 self.setupProjectKeys(source, project)
2212
2213 def setupProjectKeys(self, source, project):
2214 # Make sure we set up an RSA key for the project so that we
2215 # don't spend time generating one:
2216
James E. Blair6459db12017-06-29 14:57:20 -07002217 if isinstance(project, dict):
2218 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002219 key_root = os.path.join(self.state_root, 'keys')
2220 if not os.path.isdir(key_root):
2221 os.mkdir(key_root, 0o700)
2222 private_key_file = os.path.join(key_root, source, project + '.pem')
2223 private_key_dir = os.path.dirname(private_key_file)
2224 self.log.debug("Installing test keys for project %s at %s" % (
2225 project, private_key_file))
2226 if not os.path.isdir(private_key_dir):
2227 os.makedirs(private_key_dir)
2228 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2229 with open(private_key_file, 'w') as o:
2230 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002231
James E. Blair498059b2016-12-20 13:50:13 -08002232 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002233 self.zk_chroot_fixture = self.useFixture(
2234 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002235 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002236 self.zk_chroot_fixture.zookeeper_host,
2237 self.zk_chroot_fixture.zookeeper_port,
2238 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002239
James E. Blair96c6bf82016-01-15 16:20:40 -08002240 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002241 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002242
2243 files = {}
2244 for (dirpath, dirnames, filenames) in os.walk(source_path):
2245 for filename in filenames:
2246 test_tree_filepath = os.path.join(dirpath, filename)
2247 common_path = os.path.commonprefix([test_tree_filepath,
2248 source_path])
2249 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2250 with open(test_tree_filepath, 'r') as f:
2251 content = f.read()
2252 files[relative_filepath] = content
2253 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002254 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002255
James E. Blaire18d4602017-01-05 11:17:28 -08002256 def assertNodepoolState(self):
2257 # Make sure that there are no pending requests
2258
2259 requests = self.fake_nodepool.getNodeRequests()
2260 self.assertEqual(len(requests), 0)
2261
2262 nodes = self.fake_nodepool.getNodes()
2263 for node in nodes:
2264 self.assertFalse(node['_lock'], "Node %s is locked" %
2265 (node['_oid'],))
2266
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002267 def assertNoGeneratedKeys(self):
2268 # Make sure that Zuul did not generate any project keys
2269 # (unless it was supposed to).
2270
2271 if self.create_project_keys:
2272 return
2273
2274 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2275 test_key = i.read()
2276
2277 key_root = os.path.join(self.state_root, 'keys')
2278 for root, dirname, files in os.walk(key_root):
2279 for fn in files:
2280 with open(os.path.join(root, fn)) as f:
2281 self.assertEqual(test_key, f.read())
2282
Clark Boylanb640e052014-04-03 16:41:46 -07002283 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002284 self.log.debug("Assert final state")
2285 # Make sure no jobs are running
2286 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002287 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002288 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002289 gc.collect()
2290 for obj in gc.get_objects():
2291 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002292 self.log.debug("Leaked git repo object: 0x%x %s" %
2293 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002294 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002295 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002296 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002297 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002298 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002299 for tenant in self.sched.abide.tenants.values():
2300 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002301 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002302 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002303
2304 def shutdown(self):
2305 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002306 self.executor_server.hold_jobs_in_build = False
2307 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002308 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002309 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002310 if self.merge_server:
2311 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002312 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002313 self.sched.stop()
2314 self.sched.join()
2315 self.statsd.stop()
2316 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002317 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002318 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002319 self.fake_nodepool.stop()
2320 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002321 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002322 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002323 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002324 # Further the pydevd threads also need to be whitelisted so debugging
2325 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002326 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002327 'pydevd.CommandThread',
2328 'pydevd.Reader',
2329 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002330 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002331 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002332 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002333 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002334 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002335 log_str = ""
2336 for thread_id, stack_frame in sys._current_frames().items():
2337 log_str += "Thread: %s\n" % thread_id
2338 log_str += "".join(traceback.format_stack(stack_frame))
2339 self.log.debug(log_str)
2340 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002341
James E. Blaira002b032017-04-18 10:35:48 -07002342 def assertCleanShutdown(self):
2343 pass
2344
James E. Blairc4ba97a2017-04-19 16:26:24 -07002345 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002346 parts = project.split('/')
2347 path = os.path.join(self.upstream_root, *parts[:-1])
2348 if not os.path.exists(path):
2349 os.makedirs(path)
2350 path = os.path.join(self.upstream_root, project)
2351 repo = git.Repo.init(path)
2352
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002353 with repo.config_writer() as config_writer:
2354 config_writer.set_value('user', 'email', 'user@example.com')
2355 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002356
Clark Boylanb640e052014-04-03 16:41:46 -07002357 repo.index.commit('initial commit')
2358 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002359 if tag:
2360 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002361
James E. Blair97d902e2014-08-21 13:25:56 -07002362 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002363 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002364 repo.git.clean('-x', '-f', '-d')
2365
James E. Blair97d902e2014-08-21 13:25:56 -07002366 def create_branch(self, project, branch):
2367 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002368 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002369 fn = os.path.join(path, 'README')
2370
2371 branch_head = repo.create_head(branch)
2372 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002373 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002374 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002375 f.close()
2376 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002377 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002378
James E. Blair97d902e2014-08-21 13:25:56 -07002379 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002380 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002381 repo.git.clean('-x', '-f', '-d')
2382
James E. Blairda5bb7e2018-01-22 16:12:17 -08002383 def delete_branch(self, project, branch):
2384 path = os.path.join(self.upstream_root, project)
2385 repo = git.Repo(path)
2386 repo.head.reference = repo.heads['master']
2387 zuul.merger.merger.reset_repo_to_head(repo)
2388 repo.delete_head(repo.heads[branch], force=True)
2389
Sachi King9f16d522016-03-16 12:20:45 +11002390 def create_commit(self, project):
2391 path = os.path.join(self.upstream_root, project)
2392 repo = git.Repo(path)
2393 repo.head.reference = repo.heads['master']
2394 file_name = os.path.join(path, 'README')
2395 with open(file_name, 'a') as f:
2396 f.write('creating fake commit\n')
2397 repo.index.add([file_name])
2398 commit = repo.index.commit('Creating a fake commit')
2399 return commit.hexsha
2400
James E. Blairf4a5f022017-04-18 14:01:10 -07002401 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002402 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002403 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002404 while len(self.builds):
2405 self.release(self.builds[0])
2406 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002407 i += 1
2408 if count is not None and i >= count:
2409 break
James E. Blairb8c16472015-05-05 14:55:26 -07002410
James E. Blairdf25ddc2017-07-08 07:57:09 -07002411 def getSortedBuilds(self):
2412 "Return the list of currently running builds sorted by name"
2413
2414 return sorted(self.builds, key=lambda x: x.name)
2415
Clark Boylanb640e052014-04-03 16:41:46 -07002416 def release(self, job):
2417 if isinstance(job, FakeBuild):
2418 job.release()
2419 else:
2420 job.waiting = False
2421 self.log.debug("Queued job %s released" % job.unique)
2422 self.gearman_server.wakeConnections()
2423
2424 def getParameter(self, job, name):
2425 if isinstance(job, FakeBuild):
2426 return job.parameters[name]
2427 else:
2428 parameters = json.loads(job.arguments)
2429 return parameters[name]
2430
Clark Boylanb640e052014-04-03 16:41:46 -07002431 def haveAllBuildsReported(self):
2432 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002433 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002434 return False
2435 # Find out if every build that the worker has completed has been
2436 # reported back to Zuul. If it hasn't then that means a Gearman
2437 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002438 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002439 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002440 if not zbuild:
2441 # It has already been reported
2442 continue
2443 # It hasn't been reported yet.
2444 return False
2445 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002446 worker = self.executor_server.executor_worker
2447 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002448 if connection.state == 'GRAB_WAIT':
2449 return False
2450 return True
2451
2452 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002453 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002454 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002455 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002456 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002457 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002458 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002459 for j in conn.related_jobs.values():
2460 if j.unique == build.uuid:
2461 client_job = j
2462 break
2463 if not client_job:
2464 self.log.debug("%s is not known to the gearman client" %
2465 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002466 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002467 if not client_job.handle:
2468 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002469 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002470 server_job = self.gearman_server.jobs.get(client_job.handle)
2471 if not server_job:
2472 self.log.debug("%s is not known to the gearman server" %
2473 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002474 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002475 if not hasattr(server_job, 'waiting'):
2476 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002477 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002478 if server_job.waiting:
2479 continue
James E. Blair17302972016-08-10 16:11:42 -07002480 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002481 self.log.debug("%s has not reported start" % build)
2482 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002483 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002484 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002485 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002486 if worker_build:
2487 if worker_build.isWaiting():
2488 continue
2489 else:
2490 self.log.debug("%s is running" % worker_build)
2491 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002492 else:
James E. Blair962220f2016-08-03 11:22:38 -07002493 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002494 return False
James E. Blaira002b032017-04-18 10:35:48 -07002495 for (build_uuid, job_worker) in \
2496 self.executor_server.job_workers.items():
2497 if build_uuid not in seen_builds:
2498 self.log.debug("%s is not finalized" % build_uuid)
2499 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002500 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002501
James E. Blairdce6cea2016-12-20 16:45:32 -08002502 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002503 if self.fake_nodepool.paused:
2504 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002505 if self.sched.nodepool.requests:
2506 return False
2507 return True
2508
James E. Blaira615c362017-10-02 17:34:42 -07002509 def areAllMergeJobsWaiting(self):
2510 for client_job in list(self.merge_client.jobs):
2511 if not client_job.handle:
2512 self.log.debug("%s has no handle" % client_job)
2513 return False
2514 server_job = self.gearman_server.jobs.get(client_job.handle)
2515 if not server_job:
2516 self.log.debug("%s is not known to the gearman server" %
2517 client_job)
2518 return False
2519 if not hasattr(server_job, 'waiting'):
2520 self.log.debug("%s is being enqueued" % server_job)
2521 return False
2522 if server_job.waiting:
2523 self.log.debug("%s is waiting" % server_job)
2524 continue
2525 self.log.debug("%s is not waiting" % server_job)
2526 return False
2527 return True
2528
Jan Hruban6b71aff2015-10-22 16:58:08 +02002529 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002530 for event_queue in self.event_queues:
2531 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002532
2533 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002534 for event_queue in self.event_queues:
2535 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002536
Clark Boylanb640e052014-04-03 16:41:46 -07002537 def waitUntilSettled(self):
2538 self.log.debug("Waiting until settled...")
2539 start = time.time()
2540 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002541 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002542 self.log.error("Timeout waiting for Zuul to settle")
2543 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002544 for event_queue in self.event_queues:
2545 self.log.error(" %s: %s" %
2546 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002547 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002548 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002549 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002550 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002551 self.log.error("All requests completed: %s" %
2552 (self.areAllNodeRequestsComplete(),))
2553 self.log.error("Merge client jobs: %s" %
2554 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002555 raise Exception("Timeout waiting for Zuul to settle")
2556 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002557
Paul Belanger174a8272017-03-14 13:20:10 -04002558 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002559 # have all build states propogated to zuul?
2560 if self.haveAllBuildsReported():
2561 # Join ensures that the queue is empty _and_ events have been
2562 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002563 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002564 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002565 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002566 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002567 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002568 self.areAllNodeRequestsComplete() and
2569 all(self.eventQueuesEmpty())):
2570 # The queue empty check is placed at the end to
2571 # ensure that if a component adds an event between
2572 # when locked the run handler and checked that the
2573 # components were stable, we don't erroneously
2574 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002575 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002576 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002577 self.log.debug("...settled.")
2578 return
2579 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002580 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002581 self.sched.wake_event.wait(0.1)
2582
2583 def countJobResults(self, jobs, result):
2584 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002585 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002586
Monty Taylor0d926122017-05-24 08:07:56 -05002587 def getBuildByName(self, name):
2588 for build in self.builds:
2589 if build.name == name:
2590 return build
2591 raise Exception("Unable to find build %s" % name)
2592
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002593 def assertJobNotInHistory(self, name, project=None):
2594 for job in self.history:
2595 if (project is None or
2596 job.parameters['zuul']['project']['name'] == project):
2597 self.assertNotEqual(job.name, name,
2598 'Job %s found in history' % name)
2599
James E. Blair96c6bf82016-01-15 16:20:40 -08002600 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002601 for job in self.history:
2602 if (job.name == name and
2603 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002604 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002605 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002606 raise Exception("Unable to find job %s in history" % name)
2607
2608 def assertEmptyQueues(self):
2609 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002610 for tenant in self.sched.abide.tenants.values():
2611 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002612 for pipeline_queue in pipeline.queues:
2613 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002614 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002615 pipeline.name, pipeline_queue.name,
2616 pipeline_queue.queue))
2617 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002618 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002619
2620 def assertReportedStat(self, key, value=None, kind=None):
2621 start = time.time()
2622 while time.time() < (start + 5):
2623 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002624 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002625 if key == k:
2626 if value is None and kind is None:
2627 return
2628 elif value:
2629 if value == v:
2630 return
2631 elif kind:
2632 if v.endswith('|' + kind):
2633 return
2634 time.sleep(0.1)
2635
Clark Boylanb640e052014-04-03 16:41:46 -07002636 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002637
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002638 def assertBuilds(self, builds):
2639 """Assert that the running builds are as described.
2640
2641 The list of running builds is examined and must match exactly
2642 the list of builds described by the input.
2643
2644 :arg list builds: A list of dictionaries. Each item in the
2645 list must match the corresponding build in the build
2646 history, and each element of the dictionary must match the
2647 corresponding attribute of the build.
2648
2649 """
James E. Blair3158e282016-08-19 09:34:11 -07002650 try:
2651 self.assertEqual(len(self.builds), len(builds))
2652 for i, d in enumerate(builds):
2653 for k, v in d.items():
2654 self.assertEqual(
2655 getattr(self.builds[i], k), v,
2656 "Element %i in builds does not match" % (i,))
2657 except Exception:
2658 for build in self.builds:
2659 self.log.error("Running build: %s" % build)
2660 else:
2661 self.log.error("No running builds")
2662 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002663
James E. Blairb536ecc2016-08-31 10:11:42 -07002664 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002665 """Assert that the completed builds are as described.
2666
2667 The list of completed builds is examined and must match
2668 exactly the list of builds described by the input.
2669
2670 :arg list history: A list of dictionaries. Each item in the
2671 list must match the corresponding build in the build
2672 history, and each element of the dictionary must match the
2673 corresponding attribute of the build.
2674
James E. Blairb536ecc2016-08-31 10:11:42 -07002675 :arg bool ordered: If true, the history must match the order
2676 supplied, if false, the builds are permitted to have
2677 arrived in any order.
2678
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002679 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002680 def matches(history_item, item):
2681 for k, v in item.items():
2682 if getattr(history_item, k) != v:
2683 return False
2684 return True
James E. Blair3158e282016-08-19 09:34:11 -07002685 try:
2686 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002687 if ordered:
2688 for i, d in enumerate(history):
2689 if not matches(self.history[i], d):
2690 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002691 "Element %i in history does not match %s" %
2692 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002693 else:
2694 unseen = self.history[:]
2695 for i, d in enumerate(history):
2696 found = False
2697 for unseen_item in unseen:
2698 if matches(unseen_item, d):
2699 found = True
2700 unseen.remove(unseen_item)
2701 break
2702 if not found:
2703 raise Exception("No match found for element %i "
2704 "in history" % (i,))
2705 if unseen:
2706 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002707 except Exception:
2708 for build in self.history:
2709 self.log.error("Completed build: %s" % build)
2710 else:
2711 self.log.error("No completed builds")
2712 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002713
James E. Blair6ac368c2016-12-22 18:07:20 -08002714 def printHistory(self):
2715 """Log the build history.
2716
2717 This can be useful during tests to summarize what jobs have
2718 completed.
2719
2720 """
2721 self.log.debug("Build history:")
2722 for build in self.history:
2723 self.log.debug(build)
2724
James E. Blair59fdbac2015-12-07 17:08:06 -08002725 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002726 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2727
James E. Blair9ea70072017-04-19 16:05:30 -07002728 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002729 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002730 if not os.path.exists(root):
2731 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002732 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2733 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002734- tenant:
2735 name: openstack
2736 source:
2737 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002738 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002739 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002740 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002741 - org/project
2742 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002743 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002744 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002745 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002746 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002747 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002748
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002749 def addTagToRepo(self, project, name, sha):
2750 path = os.path.join(self.upstream_root, project)
2751 repo = git.Repo(path)
2752 repo.git.tag(name, sha)
2753
2754 def delTagFromRepo(self, project, name):
2755 path = os.path.join(self.upstream_root, project)
2756 repo = git.Repo(path)
2757 repo.git.tag('-d', name)
2758
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002759 def addCommitToRepo(self, project, message, files,
2760 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002761 path = os.path.join(self.upstream_root, project)
2762 repo = git.Repo(path)
2763 repo.head.reference = branch
2764 zuul.merger.merger.reset_repo_to_head(repo)
2765 for fn, content in files.items():
2766 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002767 try:
2768 os.makedirs(os.path.dirname(fn))
2769 except OSError:
2770 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002771 with open(fn, 'w') as f:
2772 f.write(content)
2773 repo.index.add([fn])
2774 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002775 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002776 repo.heads[branch].commit = commit
2777 repo.head.reference = branch
2778 repo.git.clean('-x', '-f', '-d')
2779 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002780 if tag:
2781 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002782 return before
2783
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002784 def commitConfigUpdate(self, project_name, source_name):
2785 """Commit an update to zuul.yaml
2786
2787 This overwrites the zuul.yaml in the specificed project with
2788 the contents specified.
2789
2790 :arg str project_name: The name of the project containing
2791 zuul.yaml (e.g., common-config)
2792
2793 :arg str source_name: The path to the file (underneath the
2794 test fixture directory) whose contents should be used to
2795 replace zuul.yaml.
2796 """
2797
2798 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002799 files = {}
2800 with open(source_path, 'r') as f:
2801 data = f.read()
2802 layout = yaml.safe_load(data)
2803 files['zuul.yaml'] = data
2804 for item in layout:
2805 if 'job' in item:
2806 jobname = item['job']['name']
2807 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002808 before = self.addCommitToRepo(
2809 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002810 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002811 return before
2812
Clint Byrum627ba362017-08-14 13:20:40 -07002813 def newTenantConfig(self, source_name):
2814 """ Use this to update the tenant config file in tests
2815
2816 This will update self.tenant_config_file to point to a temporary file
2817 for the duration of this particular test. The content of that file will
2818 be taken from FIXTURE_DIR/source_name
2819
2820 After the test the original value of self.tenant_config_file will be
2821 restored.
2822
2823 :arg str source_name: The path of the file under
2824 FIXTURE_DIR that will be used to populate the new tenant
2825 config file.
2826 """
2827 source_path = os.path.join(FIXTURE_DIR, source_name)
2828 orig_tenant_config_file = self.tenant_config_file
2829 with tempfile.NamedTemporaryFile(
2830 delete=False, mode='wb') as new_tenant_config:
2831 self.tenant_config_file = new_tenant_config.name
2832 with open(source_path, mode='rb') as source_tenant_config:
2833 new_tenant_config.write(source_tenant_config.read())
2834 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2835 self.setupAllProjectKeys()
2836 self.log.debug(
2837 'tenant_config_file = {}'.format(self.tenant_config_file))
2838
2839 def _restoreTenantConfig():
2840 self.log.debug(
2841 'restoring tenant_config_file = {}'.format(
2842 orig_tenant_config_file))
2843 os.unlink(self.tenant_config_file)
2844 self.tenant_config_file = orig_tenant_config_file
2845 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2846 self.addCleanup(_restoreTenantConfig)
2847
James E. Blair7fc8daa2016-08-08 15:37:15 -07002848 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002849
James E. Blair7fc8daa2016-08-08 15:37:15 -07002850 """Inject a Fake (Gerrit) event.
2851
2852 This method accepts a JSON-encoded event and simulates Zuul
2853 having received it from Gerrit. It could (and should)
2854 eventually apply to any connection type, but is currently only
2855 used with Gerrit connections. The name of the connection is
2856 used to look up the corresponding server, and the event is
2857 simulated as having been received by all Zuul connections
2858 attached to that server. So if two Gerrit connections in Zuul
2859 are connected to the same Gerrit server, and you invoke this
2860 method specifying the name of one of them, the event will be
2861 received by both.
2862
2863 .. note::
2864
2865 "self.fake_gerrit.addEvent" calls should be migrated to
2866 this method.
2867
2868 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002869 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002870 :arg str event: The JSON-encoded event.
2871
2872 """
2873 specified_conn = self.connections.connections[connection]
2874 for conn in self.connections.connections.values():
2875 if (isinstance(conn, specified_conn.__class__) and
2876 specified_conn.server == conn.server):
2877 conn.addEvent(event)
2878
James E. Blaird8af5422017-05-24 13:59:40 -07002879 def getUpstreamRepos(self, projects):
2880 """Return upstream git repo objects for the listed projects
2881
2882 :arg list projects: A list of strings, each the canonical name
2883 of a project.
2884
2885 :returns: A dictionary of {name: repo} for every listed
2886 project.
2887 :rtype: dict
2888
2889 """
2890
2891 repos = {}
2892 for project in projects:
2893 # FIXME(jeblair): the upstream root does not yet have a
2894 # hostname component; that needs to be added, and this
2895 # line removed:
2896 tmp_project_name = '/'.join(project.split('/')[1:])
2897 path = os.path.join(self.upstream_root, tmp_project_name)
2898 repo = git.Repo(path)
2899 repos[project] = repo
2900 return repos
2901
James E. Blair3f876d52016-07-22 13:07:14 -07002902
2903class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002904 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002905 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002906
Jamie Lennox7655b552017-03-17 12:33:38 +11002907 @contextmanager
2908 def jobLog(self, build):
2909 """Print job logs on assertion errors
2910
2911 This method is a context manager which, if it encounters an
2912 ecxeption, adds the build log to the debug output.
2913
2914 :arg Build build: The build that's being asserted.
2915 """
2916 try:
2917 yield
2918 except Exception:
2919 path = os.path.join(self.test_root, build.uuid,
2920 'work', 'logs', 'job-output.txt')
2921 with open(path) as f:
2922 self.log.debug(f.read())
2923 raise
2924
Joshua Heskethd78b4482015-09-14 16:56:34 -06002925
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002926class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002927 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002928 use_ssl = True
2929
2930
Joshua Heskethd78b4482015-09-14 16:56:34 -06002931class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002932 def setup_config(self):
2933 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002934 for section_name in self.config.sections():
2935 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2936 section_name, re.I)
2937 if not con_match:
2938 continue
2939
2940 if self.config.get(section_name, 'driver') == 'sql':
2941 f = MySQLSchemaFixture()
2942 self.useFixture(f)
2943 if (self.config.get(section_name, 'dburi') ==
2944 '$MYSQL_FIXTURE_DBURI$'):
2945 self.config.set(section_name, 'dburi', f.dburi)