blob: 036515d31199a9e255a2145915c246f5eeee1df3 [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 -070043
44import git
45import gear
46import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080047import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080048import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060049import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070050import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080051import testtools.content
52import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080053from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000054import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070055
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
60import zuul.webapp
Paul Belanger174a8272017-03-14 13:20:10 -040061import zuul.executor.server
62import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080063import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070064import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070065import zuul.merger.merger
66import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020067import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070068import zuul.nodepool
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
Clark Boylanb640e052014-04-03 16:41:46 -070072
73FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
74 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080075
76KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070077
Clark Boylanb640e052014-04-03 16:41:46 -070078
79def repack_repo(path):
80 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
81 output = subprocess.Popen(cmd, close_fds=True,
82 stdout=subprocess.PIPE,
83 stderr=subprocess.PIPE)
84 out = output.communicate()
85 if output.returncode:
86 raise Exception("git repack returned %d" % output.returncode)
87 return out
88
89
90def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040091 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070092
93
James E. Blaira190f3b2015-01-05 14:56:54 -080094def iterate_timeout(max_seconds, purpose):
95 start = time.time()
96 count = 0
97 while (time.time() < start + max_seconds):
98 count += 1
99 yield count
100 time.sleep(0)
101 raise Exception("Timeout waiting for %s" % purpose)
102
103
Jesse Keating436a5452017-04-20 11:48:41 -0700104def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700105 """Specify a layout file for use by a test method.
106
107 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700108 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700109
110 Some tests require only a very simple configuration. For those,
111 establishing a complete config directory hierachy is too much
112 work. In those cases, you can add a simple zuul.yaml file to the
113 test fixtures directory (in fixtures/layouts/foo.yaml) and use
114 this decorator to indicate the test method should use that rather
115 than the tenant config file specified by the test class.
116
117 The decorator will cause that layout file to be added to a
118 config-project called "common-config" and each "project" instance
119 referenced in the layout file will have a git repo automatically
120 initialized.
121 """
122
123 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700124 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700125 return test
126 return decorator
127
128
Gregory Haynes4fc12542015-04-22 20:38:06 -0700129class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700130 _common_path_default = "refs/changes"
131 _points_to_commits_only = True
132
133
Gregory Haynes4fc12542015-04-22 20:38:06 -0700134class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200135 categories = {'Approved': ('Approved', -1, 1),
136 'Code-Review': ('Code-Review', -2, 2),
137 'Verified': ('Verified', -2, 2)}
138
Clark Boylanb640e052014-04-03 16:41:46 -0700139 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700140 status='NEW', upstream_root=None, files={},
141 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700142 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700143 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700144 self.reported = 0
145 self.queried = 0
146 self.patchsets = []
147 self.number = number
148 self.project = project
149 self.branch = branch
150 self.subject = subject
151 self.latest_patchset = 0
152 self.depends_on_change = None
153 self.needed_by_changes = []
154 self.fail_merge = False
155 self.messages = []
156 self.data = {
157 'branch': branch,
158 'comments': [],
159 'commitMessage': subject,
160 'createdOn': time.time(),
161 'id': 'I' + random_sha1(),
162 'lastUpdated': time.time(),
163 'number': str(number),
164 'open': status == 'NEW',
165 'owner': {'email': 'user@example.com',
166 'name': 'User Name',
167 'username': 'username'},
168 'patchSets': self.patchsets,
169 'project': project,
170 'status': status,
171 'subject': subject,
172 'submitRecords': [],
173 'url': 'https://hostname/%s' % number}
174
175 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700176 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700177 self.data['submitRecords'] = self.getSubmitRecords()
178 self.open = status == 'NEW'
179
James E. Blair289f5932017-07-27 15:02:29 -0700180 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700181 path = os.path.join(self.upstream_root, self.project)
182 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700183 if parent is None:
184 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700185 ref = GerritChangeReference.create(
186 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700187 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700188 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700189 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700190 repo.git.clean('-x', '-f', '-d')
191
192 path = os.path.join(self.upstream_root, self.project)
193 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700194 for fn, content in files.items():
195 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700196 if content is None:
197 os.unlink(fn)
198 repo.index.remove([fn])
199 else:
200 d = os.path.dirname(fn)
201 if not os.path.exists(d):
202 os.makedirs(d)
203 with open(fn, 'w') as f:
204 f.write(content)
205 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700206 else:
207 for fni in range(100):
208 fn = os.path.join(path, str(fni))
209 f = open(fn, 'w')
210 for ci in range(4096):
211 f.write(random.choice(string.printable))
212 f.close()
213 repo.index.add([fn])
214
215 r = repo.index.commit(msg)
216 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700217 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700218 repo.git.clean('-x', '-f', '-d')
219 repo.heads['master'].checkout()
220 return r
221
James E. Blair289f5932017-07-27 15:02:29 -0700222 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700223 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700224 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700225 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700226 data = ("test %s %s %s\n" %
227 (self.branch, self.number, self.latest_patchset))
228 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700229 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700230 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700231 ps_files = [{'file': '/COMMIT_MSG',
232 'type': 'ADDED'},
233 {'file': 'README',
234 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700235 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700236 ps_files.append({'file': f, 'type': 'ADDED'})
237 d = {'approvals': [],
238 'createdOn': time.time(),
239 'files': ps_files,
240 'number': str(self.latest_patchset),
241 'ref': 'refs/changes/1/%s/%s' % (self.number,
242 self.latest_patchset),
243 'revision': c.hexsha,
244 'uploader': {'email': 'user@example.com',
245 'name': 'User name',
246 'username': 'user'}}
247 self.data['currentPatchSet'] = d
248 self.patchsets.append(d)
249 self.data['submitRecords'] = self.getSubmitRecords()
250
251 def getPatchsetCreatedEvent(self, patchset):
252 event = {"type": "patchset-created",
253 "change": {"project": self.project,
254 "branch": self.branch,
255 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
256 "number": str(self.number),
257 "subject": self.subject,
258 "owner": {"name": "User Name"},
259 "url": "https://hostname/3"},
260 "patchSet": self.patchsets[patchset - 1],
261 "uploader": {"name": "User Name"}}
262 return event
263
264 def getChangeRestoredEvent(self):
265 event = {"type": "change-restored",
266 "change": {"project": self.project,
267 "branch": self.branch,
268 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
269 "number": str(self.number),
270 "subject": self.subject,
271 "owner": {"name": "User Name"},
272 "url": "https://hostname/3"},
273 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100274 "patchSet": self.patchsets[-1],
275 "reason": ""}
276 return event
277
278 def getChangeAbandonedEvent(self):
279 event = {"type": "change-abandoned",
280 "change": {"project": self.project,
281 "branch": self.branch,
282 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
283 "number": str(self.number),
284 "subject": self.subject,
285 "owner": {"name": "User Name"},
286 "url": "https://hostname/3"},
287 "abandoner": {"name": "User Name"},
288 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700289 "reason": ""}
290 return event
291
292 def getChangeCommentEvent(self, patchset):
293 event = {"type": "comment-added",
294 "change": {"project": self.project,
295 "branch": self.branch,
296 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
297 "number": str(self.number),
298 "subject": self.subject,
299 "owner": {"name": "User Name"},
300 "url": "https://hostname/3"},
301 "patchSet": self.patchsets[patchset - 1],
302 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200303 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700304 "description": "Code-Review",
305 "value": "0"}],
306 "comment": "This is a comment"}
307 return event
308
James E. Blairc2a5ed72017-02-20 14:12:01 -0500309 def getChangeMergedEvent(self):
310 event = {"submitter": {"name": "Jenkins",
311 "username": "jenkins"},
312 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
313 "patchSet": self.patchsets[-1],
314 "change": self.data,
315 "type": "change-merged",
316 "eventCreatedOn": 1487613810}
317 return event
318
James E. Blair8cce42e2016-10-18 08:18:36 -0700319 def getRefUpdatedEvent(self):
320 path = os.path.join(self.upstream_root, self.project)
321 repo = git.Repo(path)
322 oldrev = repo.heads[self.branch].commit.hexsha
323
324 event = {
325 "type": "ref-updated",
326 "submitter": {
327 "name": "User Name",
328 },
329 "refUpdate": {
330 "oldRev": oldrev,
331 "newRev": self.patchsets[-1]['revision'],
332 "refName": self.branch,
333 "project": self.project,
334 }
335 }
336 return event
337
Joshua Hesketh642824b2014-07-01 17:54:59 +1000338 def addApproval(self, category, value, username='reviewer_john',
339 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700340 if not granted_on:
341 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000342 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200343 'description': self.categories[category][0],
344 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000345 'value': str(value),
346 'by': {
347 'username': username,
348 'email': username + '@example.com',
349 },
350 'grantedOn': int(granted_on)
351 }
Clark Boylanb640e052014-04-03 16:41:46 -0700352 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200353 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700354 del self.patchsets[-1]['approvals'][i]
355 self.patchsets[-1]['approvals'].append(approval)
356 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000357 'author': {'email': 'author@example.com',
358 'name': 'Patchset Author',
359 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700360 'change': {'branch': self.branch,
361 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
362 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000363 'owner': {'email': 'owner@example.com',
364 'name': 'Change Owner',
365 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700366 'project': self.project,
367 'subject': self.subject,
368 'topic': 'master',
369 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000370 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700371 'patchSet': self.patchsets[-1],
372 'type': 'comment-added'}
373 self.data['submitRecords'] = self.getSubmitRecords()
374 return json.loads(json.dumps(event))
375
376 def getSubmitRecords(self):
377 status = {}
378 for cat in self.categories.keys():
379 status[cat] = 0
380
381 for a in self.patchsets[-1]['approvals']:
382 cur = status[a['type']]
383 cat_min, cat_max = self.categories[a['type']][1:]
384 new = int(a['value'])
385 if new == cat_min:
386 cur = new
387 elif abs(new) > abs(cur):
388 cur = new
389 status[a['type']] = cur
390
391 labels = []
392 ok = True
393 for typ, cat in self.categories.items():
394 cur = status[typ]
395 cat_min, cat_max = cat[1:]
396 if cur == cat_min:
397 value = 'REJECT'
398 ok = False
399 elif cur == cat_max:
400 value = 'OK'
401 else:
402 value = 'NEED'
403 ok = False
404 labels.append({'label': cat[0], 'status': value})
405 if ok:
406 return [{'status': 'OK'}]
407 return [{'status': 'NOT_READY',
408 'labels': labels}]
409
410 def setDependsOn(self, other, patchset):
411 self.depends_on_change = other
412 d = {'id': other.data['id'],
413 'number': other.data['number'],
414 'ref': other.patchsets[patchset - 1]['ref']
415 }
416 self.data['dependsOn'] = [d]
417
418 other.needed_by_changes.append(self)
419 needed = other.data.get('neededBy', [])
420 d = {'id': self.data['id'],
421 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700422 'ref': self.patchsets[-1]['ref'],
423 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700424 }
425 needed.append(d)
426 other.data['neededBy'] = needed
427
428 def query(self):
429 self.queried += 1
430 d = self.data.get('dependsOn')
431 if d:
432 d = d[0]
433 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
434 d['isCurrentPatchSet'] = True
435 else:
436 d['isCurrentPatchSet'] = False
437 return json.loads(json.dumps(self.data))
438
439 def setMerged(self):
440 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000441 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700442 return
443 if self.fail_merge:
444 return
445 self.data['status'] = 'MERGED'
446 self.open = False
447
448 path = os.path.join(self.upstream_root, self.project)
449 repo = git.Repo(path)
450 repo.heads[self.branch].commit = \
451 repo.commit(self.patchsets[-1]['revision'])
452
453 def setReported(self):
454 self.reported += 1
455
456
James E. Blaire511d2f2016-12-08 15:22:26 -0800457class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700458 """A Fake Gerrit connection for use in tests.
459
460 This subclasses
461 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
462 ability for tests to add changes to the fake Gerrit it represents.
463 """
464
Joshua Hesketh352264b2015-08-11 23:42:08 +1000465 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700466
James E. Blaire511d2f2016-12-08 15:22:26 -0800467 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700468 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800469 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000470 connection_config)
471
Monty Taylorb934c1a2017-06-16 19:31:47 -0500472 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700473 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
474 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000475 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700476 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200477 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700478
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700479 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700480 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700481 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700482 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700483 c = FakeGerritChange(self, self.change_number, project, branch,
484 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700485 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700486 self.changes[self.change_number] = c
487 return c
488
James E. Blair72facdc2017-08-17 10:29:12 -0700489 def getFakeBranchCreatedEvent(self, project, branch):
490 path = os.path.join(self.upstream_root, project)
491 repo = git.Repo(path)
492 oldrev = 40 * '0'
493
494 event = {
495 "type": "ref-updated",
496 "submitter": {
497 "name": "User Name",
498 },
499 "refUpdate": {
500 "oldRev": oldrev,
501 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700502 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700503 "project": project,
504 }
505 }
506 return event
507
Clark Boylanb640e052014-04-03 16:41:46 -0700508 def review(self, project, changeid, message, action):
509 number, ps = changeid.split(',')
510 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000511
512 # Add the approval back onto the change (ie simulate what gerrit would
513 # do).
514 # Usually when zuul leaves a review it'll create a feedback loop where
515 # zuul's review enters another gerrit event (which is then picked up by
516 # zuul). However, we can't mimic this behaviour (by adding this
517 # approval event into the queue) as it stops jobs from checking what
518 # happens before this event is triggered. If a job needs to see what
519 # happens they can add their own verified event into the queue.
520 # Nevertheless, we can update change with the new review in gerrit.
521
James E. Blair8b5408c2016-08-08 15:37:46 -0700522 for cat in action.keys():
523 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000524 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000525
Clark Boylanb640e052014-04-03 16:41:46 -0700526 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000527
Clark Boylanb640e052014-04-03 16:41:46 -0700528 if 'submit' in action:
529 change.setMerged()
530 if message:
531 change.setReported()
532
533 def query(self, number):
534 change = self.changes.get(int(number))
535 if change:
536 return change.query()
537 return {}
538
James E. Blairc494d542014-08-06 09:23:52 -0700539 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700540 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700541 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800542 if query.startswith('change:'):
543 # Query a specific changeid
544 changeid = query[len('change:'):]
545 l = [change.query() for change in self.changes.values()
546 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700547 elif query.startswith('message:'):
548 # Query the content of a commit message
549 msg = query[len('message:'):].strip()
550 l = [change.query() for change in self.changes.values()
551 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800552 else:
553 # Query all open changes
554 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700555 return l
James E. Blairc494d542014-08-06 09:23:52 -0700556
Joshua Hesketh352264b2015-08-11 23:42:08 +1000557 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700558 pass
559
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200560 def _uploadPack(self, project):
561 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
562 'multi_ack thin-pack side-band side-band-64k ofs-delta '
563 'shallow no-progress include-tag multi_ack_detailed no-done\n')
564 path = os.path.join(self.upstream_root, project.name)
565 repo = git.Repo(path)
566 for ref in repo.refs:
567 r = ref.object.hexsha + ' ' + ref.path + '\n'
568 ret += '%04x%s' % (len(r) + 4, r)
569 ret += '0000'
570 return ret
571
Joshua Hesketh352264b2015-08-11 23:42:08 +1000572 def getGitUrl(self, project):
573 return os.path.join(self.upstream_root, project.name)
574
Clark Boylanb640e052014-04-03 16:41:46 -0700575
Gregory Haynes4fc12542015-04-22 20:38:06 -0700576class GithubChangeReference(git.Reference):
577 _common_path_default = "refs/pull"
578 _points_to_commits_only = True
579
580
Tobias Henkel64e37a02017-08-02 10:13:30 +0200581class FakeGithub(object):
582
583 class FakeUser(object):
584 def __init__(self, login):
585 self.login = login
586 self.name = "Github User"
587 self.email = "github.user@example.com"
588
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200589 class FakeBranch(object):
590 def __init__(self, branch='master'):
591 self.name = branch
592
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200593 class FakeStatus(object):
594 def __init__(self, state, url, description, context, user):
595 self._state = state
596 self._url = url
597 self._description = description
598 self._context = context
599 self._user = user
600
601 def as_dict(self):
602 return {
603 'state': self._state,
604 'url': self._url,
605 'description': self._description,
606 'context': self._context,
607 'creator': {
608 'login': self._user
609 }
610 }
611
612 class FakeCommit(object):
613 def __init__(self):
614 self._statuses = []
615
616 def set_status(self, state, url, description, context, user):
617 status = FakeGithub.FakeStatus(
618 state, url, description, context, user)
619 # always insert a status to the front of the list, to represent
620 # the last status provided for a commit.
621 self._statuses.insert(0, status)
622
623 def statuses(self):
624 return self._statuses
625
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200626 class FakeRepository(object):
627 def __init__(self):
628 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200629 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200630
Tobias Henkeleca46202017-08-02 20:27:10 +0200631 def branches(self, protected=False):
632 if protected:
633 # simulate there is no protected branch
634 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200635 return self._branches
636
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200637 def create_status(self, sha, state, url, description, context,
638 user='zuul'):
639 # Since we're bypassing github API, which would require a user, we
640 # default the user as 'zuul' here.
641 commit = self._commits.get(sha, None)
642 if commit is None:
643 commit = FakeGithub.FakeCommit()
644 self._commits[sha] = commit
645 commit.set_status(state, url, description, context, user)
646
647 def commit(self, sha):
648 commit = self._commits.get(sha, None)
649 if commit is None:
650 commit = FakeGithub.FakeCommit()
651 self._commits[sha] = commit
652 return commit
653
654 def __init__(self):
655 self._repos = {}
656
Tobias Henkel64e37a02017-08-02 10:13:30 +0200657 def user(self, login):
658 return self.FakeUser(login)
659
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200660 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200661 return self._repos.get((owner, proj), None)
662
663 def repo_from_project(self, project):
664 # This is a convenience method for the tests.
665 owner, proj = project.split('/')
666 return self.repository(owner, proj)
667
668 def addProject(self, project):
669 owner, proj = project.name.split('/')
670 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200671
Tobias Henkel64e37a02017-08-02 10:13:30 +0200672
Gregory Haynes4fc12542015-04-22 20:38:06 -0700673class FakeGithubPullRequest(object):
674
675 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800676 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700677 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700678 """Creates a new PR with several commits.
679 Sends an event about opened PR."""
680 self.github = github
681 self.source = github
682 self.number = number
683 self.project = project
684 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100685 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700686 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100687 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700688 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100689 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700690 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100691 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100692 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800693 self.reviews = []
694 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700695 self.updated_at = None
696 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100697 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100698 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700699 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700700 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100701 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700702 self._updateTimeStamp()
703
Jan Hruban570d01c2016-03-10 21:51:32 +0100704 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700705 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100706 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700707 self._updateTimeStamp()
708
Jan Hruban570d01c2016-03-10 21:51:32 +0100709 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700710 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100711 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700712 self._updateTimeStamp()
713
714 def getPullRequestOpenedEvent(self):
715 return self._getPullRequestEvent('opened')
716
717 def getPullRequestSynchronizeEvent(self):
718 return self._getPullRequestEvent('synchronize')
719
720 def getPullRequestReopenedEvent(self):
721 return self._getPullRequestEvent('reopened')
722
723 def getPullRequestClosedEvent(self):
724 return self._getPullRequestEvent('closed')
725
Jesse Keatinga41566f2017-06-14 18:17:51 -0700726 def getPullRequestEditedEvent(self):
727 return self._getPullRequestEvent('edited')
728
Gregory Haynes4fc12542015-04-22 20:38:06 -0700729 def addComment(self, message):
730 self.comments.append(message)
731 self._updateTimeStamp()
732
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200733 def getCommentAddedEvent(self, text):
734 name = 'issue_comment'
735 data = {
736 'action': 'created',
737 'issue': {
738 'number': self.number
739 },
740 'comment': {
741 'body': text
742 },
743 'repository': {
744 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100745 },
746 'sender': {
747 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200748 }
749 }
750 return (name, data)
751
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800752 def getReviewAddedEvent(self, review):
753 name = 'pull_request_review'
754 data = {
755 'action': 'submitted',
756 'pull_request': {
757 'number': self.number,
758 'title': self.subject,
759 'updated_at': self.updated_at,
760 'base': {
761 'ref': self.branch,
762 'repo': {
763 'full_name': self.project
764 }
765 },
766 'head': {
767 'sha': self.head_sha
768 }
769 },
770 'review': {
771 'state': review
772 },
773 'repository': {
774 'full_name': self.project
775 },
776 'sender': {
777 'login': 'ghuser'
778 }
779 }
780 return (name, data)
781
Jan Hruban16ad31f2015-11-07 14:39:07 +0100782 def addLabel(self, name):
783 if name not in self.labels:
784 self.labels.append(name)
785 self._updateTimeStamp()
786 return self._getLabelEvent(name)
787
788 def removeLabel(self, name):
789 if name in self.labels:
790 self.labels.remove(name)
791 self._updateTimeStamp()
792 return self._getUnlabelEvent(name)
793
794 def _getLabelEvent(self, label):
795 name = 'pull_request'
796 data = {
797 'action': 'labeled',
798 'pull_request': {
799 'number': self.number,
800 'updated_at': self.updated_at,
801 'base': {
802 'ref': self.branch,
803 'repo': {
804 'full_name': self.project
805 }
806 },
807 'head': {
808 'sha': self.head_sha
809 }
810 },
811 'label': {
812 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100813 },
814 'sender': {
815 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100816 }
817 }
818 return (name, data)
819
820 def _getUnlabelEvent(self, label):
821 name = 'pull_request'
822 data = {
823 'action': 'unlabeled',
824 'pull_request': {
825 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100826 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100827 'updated_at': self.updated_at,
828 'base': {
829 'ref': self.branch,
830 'repo': {
831 'full_name': self.project
832 }
833 },
834 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800835 'sha': self.head_sha,
836 'repo': {
837 'full_name': self.project
838 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100839 }
840 },
841 'label': {
842 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100843 },
844 'sender': {
845 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100846 }
847 }
848 return (name, data)
849
Jesse Keatinga41566f2017-06-14 18:17:51 -0700850 def editBody(self, body):
851 self.body = body
852 self._updateTimeStamp()
853
Gregory Haynes4fc12542015-04-22 20:38:06 -0700854 def _getRepo(self):
855 repo_path = os.path.join(self.upstream_root, self.project)
856 return git.Repo(repo_path)
857
858 def _createPRRef(self):
859 repo = self._getRepo()
860 GithubChangeReference.create(
861 repo, self._getPRReference(), 'refs/tags/init')
862
Jan Hruban570d01c2016-03-10 21:51:32 +0100863 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700864 repo = self._getRepo()
865 ref = repo.references[self._getPRReference()]
866 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100867 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700868 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100869 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700870 repo.head.reference = ref
871 zuul.merger.merger.reset_repo_to_head(repo)
872 repo.git.clean('-x', '-f', '-d')
873
Jan Hruban570d01c2016-03-10 21:51:32 +0100874 if files:
875 fn = files[0]
876 self.files = files
877 else:
878 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
879 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100880 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700881 fn = os.path.join(repo.working_dir, fn)
882 f = open(fn, 'w')
883 with open(fn, 'w') as f:
884 f.write("test %s %s\n" %
885 (self.branch, self.number))
886 repo.index.add([fn])
887
888 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800889 # Create an empty set of statuses for the given sha,
890 # each sha on a PR may have a status set on it
891 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700892 repo.head.reference = 'master'
893 zuul.merger.merger.reset_repo_to_head(repo)
894 repo.git.clean('-x', '-f', '-d')
895 repo.heads['master'].checkout()
896
897 def _updateTimeStamp(self):
898 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
899
900 def getPRHeadSha(self):
901 repo = self._getRepo()
902 return repo.references[self._getPRReference()].commit.hexsha
903
Jesse Keatingae4cd272017-01-30 17:10:44 -0800904 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800905 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
906 # convert the timestamp to a str format that would be returned
907 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800908
Adam Gandelmand81dd762017-02-09 15:15:49 -0800909 if granted_on:
910 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
911 submitted_at = time.strftime(
912 gh_time_format, granted_on.timetuple())
913 else:
914 # github timestamps only down to the second, so we need to make
915 # sure reviews that tests add appear to be added over a period of
916 # time in the past and not all at once.
917 if not self.reviews:
918 # the first review happens 10 mins ago
919 offset = 600
920 else:
921 # subsequent reviews happen 1 minute closer to now
922 offset = 600 - (len(self.reviews) * 60)
923
924 granted_on = datetime.datetime.utcfromtimestamp(
925 time.time() - offset)
926 submitted_at = time.strftime(
927 gh_time_format, granted_on.timetuple())
928
Jesse Keatingae4cd272017-01-30 17:10:44 -0800929 self.reviews.append({
930 'state': state,
931 'user': {
932 'login': user,
933 'email': user + "@derp.com",
934 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800935 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800936 })
937
Gregory Haynes4fc12542015-04-22 20:38:06 -0700938 def _getPRReference(self):
939 return '%s/head' % self.number
940
941 def _getPullRequestEvent(self, action):
942 name = 'pull_request'
943 data = {
944 'action': action,
945 'number': self.number,
946 'pull_request': {
947 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100948 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700949 'updated_at': self.updated_at,
950 'base': {
951 'ref': self.branch,
952 'repo': {
953 'full_name': self.project
954 }
955 },
956 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800957 'sha': self.head_sha,
958 'repo': {
959 'full_name': self.project
960 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700961 },
962 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100963 },
964 'sender': {
965 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700966 }
967 }
968 return (name, data)
969
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800970 def getCommitStatusEvent(self, context, state='success', user='zuul'):
971 name = 'status'
972 data = {
973 'state': state,
974 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700975 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800976 'description': 'Test results for %s: %s' % (self.head_sha, state),
977 'target_url': 'http://zuul/%s' % self.head_sha,
978 'branches': [],
979 'context': context,
980 'sender': {
981 'login': user
982 }
983 }
984 return (name, data)
985
James E. Blair289f5932017-07-27 15:02:29 -0700986 def setMerged(self, commit_message):
987 self.is_merged = True
988 self.merge_message = commit_message
989
990 repo = self._getRepo()
991 repo.heads[self.branch].commit = repo.commit(self.head_sha)
992
Gregory Haynes4fc12542015-04-22 20:38:06 -0700993
994class FakeGithubConnection(githubconnection.GithubConnection):
995 log = logging.getLogger("zuul.test.FakeGithubConnection")
996
997 def __init__(self, driver, connection_name, connection_config,
998 upstream_root=None):
999 super(FakeGithubConnection, self).__init__(driver, connection_name,
1000 connection_config)
1001 self.connection_name = connection_name
1002 self.pr_number = 0
1003 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001004 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001005 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001006 self.merge_failure = False
1007 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001008 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001009 self.github_client = FakeGithub()
1010
1011 def getGithubClient(self,
1012 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -06001013 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001014 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001015
Jesse Keatinga41566f2017-06-14 18:17:51 -07001016 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001017 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001018 self.pr_number += 1
1019 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001020 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001021 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001022 self.pull_requests.append(pull_request)
1023 return pull_request
1024
Jesse Keating71a47ff2017-06-06 11:36:43 -07001025 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1026 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001027 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001028 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001029 if not new_rev:
1030 new_rev = random_sha1()
1031 name = 'push'
1032 data = {
1033 'ref': ref,
1034 'before': old_rev,
1035 'after': new_rev,
1036 'repository': {
1037 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001038 },
1039 'commits': [
1040 {
1041 'added': added_files,
1042 'removed': removed_files,
1043 'modified': modified_files
1044 }
1045 ]
Wayne1a78c612015-06-11 17:14:13 -07001046 }
1047 return (name, data)
1048
Gregory Haynes4fc12542015-04-22 20:38:06 -07001049 def emitEvent(self, event):
1050 """Emulates sending the GitHub webhook event to the connection."""
1051 port = self.webapp.server.socket.getsockname()[1]
1052 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001053 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001054 secret = self.connection_config['webhook_token']
1055 signature = githubconnection._sign_request(payload, secret)
1056 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001057 req = urllib.request.Request(
1058 'http://localhost:%s/connection/%s/payload'
1059 % (port, self.connection_name),
1060 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001061 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001062
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001063 def addProject(self, project):
1064 # use the original method here and additionally register it in the
1065 # fake github
1066 super(FakeGithubConnection, self).addProject(project)
1067 self.getGithubClient(project).addProject(project)
1068
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001069 def getPull(self, project, number):
1070 pr = self.pull_requests[number - 1]
1071 data = {
1072 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001073 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001074 'updated_at': pr.updated_at,
1075 'base': {
1076 'repo': {
1077 'full_name': pr.project
1078 },
1079 'ref': pr.branch,
1080 },
Jan Hruban37615e52015-11-19 14:30:49 +01001081 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001082 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001083 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001084 'sha': pr.head_sha,
1085 'repo': {
1086 'full_name': pr.project
1087 }
Jesse Keating61040e72017-06-08 15:08:27 -07001088 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001089 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001090 'labels': pr.labels,
1091 'merged': pr.is_merged,
1092 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001093 }
1094 return data
1095
Jesse Keating9021a012017-08-29 14:45:27 -07001096 def getPullBySha(self, sha, project):
1097 prs = list(set([p for p in self.pull_requests if
1098 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001099 if len(prs) > 1:
1100 raise Exception('Multiple pulls found with head sha: %s' % sha)
1101 pr = prs[0]
1102 return self.getPull(pr.project, pr.number)
1103
Jesse Keatingae4cd272017-01-30 17:10:44 -08001104 def _getPullReviews(self, owner, project, number):
1105 pr = self.pull_requests[number - 1]
1106 return pr.reviews
1107
Jesse Keatingae4cd272017-01-30 17:10:44 -08001108 def getRepoPermission(self, project, login):
1109 owner, proj = project.split('/')
1110 for pr in self.pull_requests:
1111 pr_owner, pr_project = pr.project.split('/')
1112 if (pr_owner == owner and proj == pr_project):
1113 if login in pr.writers:
1114 return 'write'
1115 else:
1116 return 'read'
1117
Gregory Haynes4fc12542015-04-22 20:38:06 -07001118 def getGitUrl(self, project):
1119 return os.path.join(self.upstream_root, str(project))
1120
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001121 def real_getGitUrl(self, project):
1122 return super(FakeGithubConnection, self).getGitUrl(project)
1123
Jan Hrubane252a732017-01-03 15:03:09 +01001124 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001125 # record that this got reported
1126 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001127 pull_request = self.pull_requests[pr_number - 1]
1128 pull_request.addComment(message)
1129
Jan Hruban3b415922016-02-03 13:10:22 +01001130 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001131 # record that this got reported
1132 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001133 pull_request = self.pull_requests[pr_number - 1]
1134 if self.merge_failure:
1135 raise Exception('Pull request was not merged')
1136 if self.merge_not_allowed_count > 0:
1137 self.merge_not_allowed_count -= 1
1138 raise MergeFailure('Merge was not successful due to mergeability'
1139 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001140 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001141
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001142 def setCommitStatus(self, project, sha, state, url='', description='',
1143 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001144 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001145 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001146 super(FakeGithubConnection, self).setCommitStatus(
1147 project, sha, state,
1148 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001149
Jan Hruban16ad31f2015-11-07 14:39:07 +01001150 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001151 # record that this got reported
1152 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001153 pull_request = self.pull_requests[pr_number - 1]
1154 pull_request.addLabel(label)
1155
1156 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001157 # record that this got reported
1158 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001159 pull_request = self.pull_requests[pr_number - 1]
1160 pull_request.removeLabel(label)
1161
Jesse Keatinga41566f2017-06-14 18:17:51 -07001162 def _getNeededByFromPR(self, change):
1163 prs = []
1164 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001165 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001166 change.number))
1167 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001168 if not pr.body:
1169 body = ''
1170 else:
1171 body = pr.body
1172 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001173 # Get our version of a pull so that it's a dict
1174 pull = self.getPull(pr.project, pr.number)
1175 prs.append(pull)
1176
1177 return prs
1178
Gregory Haynes4fc12542015-04-22 20:38:06 -07001179
Clark Boylanb640e052014-04-03 16:41:46 -07001180class BuildHistory(object):
1181 def __init__(self, **kw):
1182 self.__dict__.update(kw)
1183
1184 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001185 return ("<Completed build, result: %s name: %s uuid: %s "
1186 "changes: %s ref: %s>" %
1187 (self.result, self.name, self.uuid,
1188 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001189
1190
Clark Boylanb640e052014-04-03 16:41:46 -07001191class FakeStatsd(threading.Thread):
1192 def __init__(self):
1193 threading.Thread.__init__(self)
1194 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001195 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001196 self.sock.bind(('', 0))
1197 self.port = self.sock.getsockname()[1]
1198 self.wake_read, self.wake_write = os.pipe()
1199 self.stats = []
1200
1201 def run(self):
1202 while True:
1203 poll = select.poll()
1204 poll.register(self.sock, select.POLLIN)
1205 poll.register(self.wake_read, select.POLLIN)
1206 ret = poll.poll()
1207 for (fd, event) in ret:
1208 if fd == self.sock.fileno():
1209 data = self.sock.recvfrom(1024)
1210 if not data:
1211 return
1212 self.stats.append(data[0])
1213 if fd == self.wake_read:
1214 return
1215
1216 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001217 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001218
1219
James E. Blaire1767bc2016-08-02 10:00:27 -07001220class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001221 log = logging.getLogger("zuul.test")
1222
Paul Belanger174a8272017-03-14 13:20:10 -04001223 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001224 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001225 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001226 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001227 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001228 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001229 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001230 # TODOv3(jeblair): self.node is really "the label of the node
1231 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001232 # keep using it like this, or we may end up exposing more of
1233 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001234 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001235 self.node = None
1236 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001237 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001238 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001239 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001240 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001241 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001242 self.wait_condition = threading.Condition()
1243 self.waiting = False
1244 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001245 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001246 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001247 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001248 items = self.parameters['zuul']['items']
1249 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1250 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001251
James E. Blair3158e282016-08-19 09:34:11 -07001252 def __repr__(self):
1253 waiting = ''
1254 if self.waiting:
1255 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001256 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1257 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001258
Clark Boylanb640e052014-04-03 16:41:46 -07001259 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001260 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001261 self.wait_condition.acquire()
1262 self.wait_condition.notify()
1263 self.waiting = False
1264 self.log.debug("Build %s released" % self.unique)
1265 self.wait_condition.release()
1266
1267 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001268 """Return whether this build is being held.
1269
1270 :returns: Whether the build is being held.
1271 :rtype: bool
1272 """
1273
Clark Boylanb640e052014-04-03 16:41:46 -07001274 self.wait_condition.acquire()
1275 if self.waiting:
1276 ret = True
1277 else:
1278 ret = False
1279 self.wait_condition.release()
1280 return ret
1281
1282 def _wait(self):
1283 self.wait_condition.acquire()
1284 self.waiting = True
1285 self.log.debug("Build %s waiting" % self.unique)
1286 self.wait_condition.wait()
1287 self.wait_condition.release()
1288
1289 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001290 self.log.debug('Running build %s' % self.unique)
1291
Paul Belanger174a8272017-03-14 13:20:10 -04001292 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001293 self.log.debug('Holding build %s' % self.unique)
1294 self._wait()
1295 self.log.debug("Build %s continuing" % self.unique)
1296
James E. Blair412fba82017-01-26 15:00:50 -08001297 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001298 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001299 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001300 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001301 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001302 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001303 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001304
James E. Blaire1767bc2016-08-02 10:00:27 -07001305 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001306
James E. Blaira5dba232016-08-08 15:53:24 -07001307 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001308 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001309 for change in changes:
1310 if self.hasChanges(change):
1311 return True
1312 return False
1313
James E. Blaire7b99a02016-08-05 14:27:34 -07001314 def hasChanges(self, *changes):
1315 """Return whether this build has certain changes in its git repos.
1316
1317 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001318 are expected to be present (in order) in the git repository of
1319 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001320
1321 :returns: Whether the build has the indicated changes.
1322 :rtype: bool
1323
1324 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001325 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001326 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001327 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001328 try:
1329 repo = git.Repo(path)
1330 except NoSuchPathError as e:
1331 self.log.debug('%s' % e)
1332 return False
James E. Blair247cab72017-07-20 16:52:36 -07001333 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001334 commit_message = '%s-1' % change.subject
1335 self.log.debug("Checking if build %s has changes; commit_message "
1336 "%s; repo_messages %s" % (self, commit_message,
1337 repo_messages))
1338 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001339 self.log.debug(" messages do not match")
1340 return False
1341 self.log.debug(" OK")
1342 return True
1343
James E. Blaird8af5422017-05-24 13:59:40 -07001344 def getWorkspaceRepos(self, projects):
1345 """Return workspace git repo objects for the listed projects
1346
1347 :arg list projects: A list of strings, each the canonical name
1348 of a project.
1349
1350 :returns: A dictionary of {name: repo} for every listed
1351 project.
1352 :rtype: dict
1353
1354 """
1355
1356 repos = {}
1357 for project in projects:
1358 path = os.path.join(self.jobdir.src_root, project)
1359 repo = git.Repo(path)
1360 repos[project] = repo
1361 return repos
1362
Clark Boylanb640e052014-04-03 16:41:46 -07001363
James E. Blair107bb252017-10-13 15:53:16 -07001364class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1365 def doMergeChanges(self, merger, items, repo_state):
1366 # Get a merger in order to update the repos involved in this job.
1367 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1368 merger, items, repo_state)
1369 if not commit: # merge conflict
1370 self.recordResult('MERGER_FAILURE')
1371 return commit
1372
1373 def recordResult(self, result):
1374 build = self.executor_server.job_builds[self.job.unique]
1375 self.executor_server.lock.acquire()
1376 self.executor_server.build_history.append(
1377 BuildHistory(name=build.name, result=result, changes=build.changes,
1378 node=build.node, uuid=build.unique,
1379 ref=build.parameters['zuul']['ref'],
1380 parameters=build.parameters, jobdir=build.jobdir,
1381 pipeline=build.parameters['zuul']['pipeline'])
1382 )
1383 self.executor_server.running_builds.remove(build)
1384 del self.executor_server.job_builds[self.job.unique]
1385 self.executor_server.lock.release()
1386
1387 def runPlaybooks(self, args):
1388 build = self.executor_server.job_builds[self.job.unique]
1389 build.jobdir = self.jobdir
1390
1391 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1392 self.recordResult(result)
1393 return result
1394
James E. Blaira86aaf12017-10-15 20:59:50 -07001395 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001396 build = self.executor_server.job_builds[self.job.unique]
1397
1398 if self.executor_server._run_ansible:
1399 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001400 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001401 else:
1402 if playbook.path:
1403 result = build.run()
1404 else:
1405 result = (self.RESULT_NORMAL, 0)
1406 return result
1407
1408 def getHostList(self, args):
1409 self.log.debug("hostlist")
1410 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1411 for host in hosts:
1412 host['host_vars']['ansible_connection'] = 'local'
1413
1414 hosts.append(dict(
1415 name='localhost',
1416 host_vars=dict(ansible_connection='local'),
1417 host_keys=[]))
1418 return hosts
1419
1420
Paul Belanger174a8272017-03-14 13:20:10 -04001421class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1422 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001423
Paul Belanger174a8272017-03-14 13:20:10 -04001424 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001425 they will report that they have started but then pause until
1426 released before reporting completion. This attribute may be
1427 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001428 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001429 be explicitly released.
1430
1431 """
James E. Blairfaf81982017-10-10 15:42:26 -07001432
1433 _job_class = RecordingAnsibleJob
1434
James E. Blairf5dbd002015-12-23 15:26:17 -08001435 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001436 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001437 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001438 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001439 self.hold_jobs_in_build = False
1440 self.lock = threading.Lock()
1441 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001442 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001443 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001444 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001445
James E. Blaira5dba232016-08-08 15:53:24 -07001446 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001447 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001448
1449 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001450 :arg Change change: The :py:class:`~tests.base.FakeChange`
1451 instance which should cause the job to fail. This job
1452 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001453
1454 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001455 l = self.fail_tests.get(name, [])
1456 l.append(change)
1457 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001458
James E. Blair962220f2016-08-03 11:22:38 -07001459 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001460 """Release a held build.
1461
1462 :arg str regex: A regular expression which, if supplied, will
1463 cause only builds with matching names to be released. If
1464 not supplied, all builds will be released.
1465
1466 """
James E. Blair962220f2016-08-03 11:22:38 -07001467 builds = self.running_builds[:]
1468 self.log.debug("Releasing build %s (%s)" % (regex,
1469 len(self.running_builds)))
1470 for build in builds:
1471 if not regex or re.match(regex, build.name):
1472 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001473 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001474 build.release()
1475 else:
1476 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001477 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001478 self.log.debug("Done releasing builds %s (%s)" %
1479 (regex, len(self.running_builds)))
1480
Paul Belanger174a8272017-03-14 13:20:10 -04001481 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001482 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001483 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001484 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001485 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001486 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001487 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001488 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001489 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001490
1491 def stopJob(self, job):
1492 self.log.debug("handle stop")
1493 parameters = json.loads(job.arguments)
1494 uuid = parameters['uuid']
1495 for build in self.running_builds:
1496 if build.unique == uuid:
1497 build.aborted = True
1498 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001499 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001500
James E. Blaira002b032017-04-18 10:35:48 -07001501 def stop(self):
1502 for build in self.running_builds:
1503 build.release()
1504 super(RecordingExecutorServer, self).stop()
1505
Joshua Hesketh50c21782016-10-13 21:34:14 +11001506
Clark Boylanb640e052014-04-03 16:41:46 -07001507class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001508 """A Gearman server for use in tests.
1509
1510 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1511 added to the queue but will not be distributed to workers
1512 until released. This attribute may be changed at any time and
1513 will take effect for subsequently enqueued jobs, but
1514 previously held jobs will still need to be explicitly
1515 released.
1516
1517 """
1518
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001519 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001520 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001521 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001522 if use_ssl:
1523 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1524 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1525 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1526 else:
1527 ssl_ca = None
1528 ssl_cert = None
1529 ssl_key = None
1530
1531 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1532 ssl_cert=ssl_cert,
1533 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001534
1535 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001536 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1537 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001538 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001539 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001540 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001541 elif job.name.startswith(b'merger:'):
1542 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001543 else:
1544 job.waiting = False
1545 if job.waiting:
1546 continue
1547 if job.name in connection.functions:
1548 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001549 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001550 connection.related_jobs[job.handle] = job
1551 job.worker_connection = connection
1552 job.running = True
1553 return job
1554 return None
1555
1556 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001557 """Release a held job.
1558
1559 :arg str regex: A regular expression which, if supplied, will
1560 cause only jobs with matching names to be released. If
1561 not supplied, all jobs will be released.
1562 """
Clark Boylanb640e052014-04-03 16:41:46 -07001563 released = False
1564 qlen = (len(self.high_queue) + len(self.normal_queue) +
1565 len(self.low_queue))
1566 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1567 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001568 match = False
1569 if job.name == b'executor:execute':
1570 parameters = json.loads(job.arguments.decode('utf8'))
1571 if not regex or re.match(regex, parameters.get('job')):
1572 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001573 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001574 if not regex:
1575 match = True
1576 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001577 self.log.debug("releasing queued job %s" %
1578 job.unique)
1579 job.waiting = False
1580 released = True
1581 else:
1582 self.log.debug("not releasing queued job %s" %
1583 job.unique)
1584 if released:
1585 self.wakeConnections()
1586 qlen = (len(self.high_queue) + len(self.normal_queue) +
1587 len(self.low_queue))
1588 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1589
1590
1591class FakeSMTP(object):
1592 log = logging.getLogger('zuul.FakeSMTP')
1593
1594 def __init__(self, messages, server, port):
1595 self.server = server
1596 self.port = port
1597 self.messages = messages
1598
1599 def sendmail(self, from_email, to_email, msg):
1600 self.log.info("Sending email from %s, to %s, with msg %s" % (
1601 from_email, to_email, msg))
1602
1603 headers = msg.split('\n\n', 1)[0]
1604 body = msg.split('\n\n', 1)[1]
1605
1606 self.messages.append(dict(
1607 from_email=from_email,
1608 to_email=to_email,
1609 msg=msg,
1610 headers=headers,
1611 body=body,
1612 ))
1613
1614 return True
1615
1616 def quit(self):
1617 return True
1618
1619
James E. Blairdce6cea2016-12-20 16:45:32 -08001620class FakeNodepool(object):
1621 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001622 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001623
1624 log = logging.getLogger("zuul.test.FakeNodepool")
1625
1626 def __init__(self, host, port, chroot):
1627 self.client = kazoo.client.KazooClient(
1628 hosts='%s:%s%s' % (host, port, chroot))
1629 self.client.start()
1630 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001631 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001632 self.thread = threading.Thread(target=self.run)
1633 self.thread.daemon = True
1634 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001635 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001636
1637 def stop(self):
1638 self._running = False
1639 self.thread.join()
1640 self.client.stop()
1641 self.client.close()
1642
1643 def run(self):
1644 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001645 try:
1646 self._run()
1647 except Exception:
1648 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001649 time.sleep(0.1)
1650
1651 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001652 if self.paused:
1653 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001654 for req in self.getNodeRequests():
1655 self.fulfillRequest(req)
1656
1657 def getNodeRequests(self):
1658 try:
1659 reqids = self.client.get_children(self.REQUEST_ROOT)
1660 except kazoo.exceptions.NoNodeError:
1661 return []
1662 reqs = []
1663 for oid in sorted(reqids):
1664 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001665 try:
1666 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001667 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001668 data['_oid'] = oid
1669 reqs.append(data)
1670 except kazoo.exceptions.NoNodeError:
1671 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001672 return reqs
1673
James E. Blaire18d4602017-01-05 11:17:28 -08001674 def getNodes(self):
1675 try:
1676 nodeids = self.client.get_children(self.NODE_ROOT)
1677 except kazoo.exceptions.NoNodeError:
1678 return []
1679 nodes = []
1680 for oid in sorted(nodeids):
1681 path = self.NODE_ROOT + '/' + oid
1682 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001683 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001684 data['_oid'] = oid
1685 try:
1686 lockfiles = self.client.get_children(path + '/lock')
1687 except kazoo.exceptions.NoNodeError:
1688 lockfiles = []
1689 if lockfiles:
1690 data['_lock'] = True
1691 else:
1692 data['_lock'] = False
1693 nodes.append(data)
1694 return nodes
1695
James E. Blaira38c28e2017-01-04 10:33:20 -08001696 def makeNode(self, request_id, node_type):
1697 now = time.time()
1698 path = '/nodepool/nodes/'
1699 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001700 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001701 provider='test-provider',
1702 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001703 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001704 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001705 public_ipv4='127.0.0.1',
1706 private_ipv4=None,
1707 public_ipv6=None,
1708 allocated_to=request_id,
1709 state='ready',
1710 state_time=now,
1711 created_time=now,
1712 updated_time=now,
1713 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001714 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001715 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001716 if 'fakeuser' in node_type:
1717 data['username'] = 'fakeuser'
Clint Byrumf322fe22017-05-10 20:53:12 -07001718 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001719 path = self.client.create(path, data,
1720 makepath=True,
1721 sequence=True)
1722 nodeid = path.split("/")[-1]
1723 return nodeid
1724
James E. Blair6ab79e02017-01-06 10:10:17 -08001725 def addFailRequest(self, request):
1726 self.fail_requests.add(request['_oid'])
1727
James E. Blairdce6cea2016-12-20 16:45:32 -08001728 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001729 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001730 return
1731 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001732 oid = request['_oid']
1733 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001734
James E. Blair6ab79e02017-01-06 10:10:17 -08001735 if oid in self.fail_requests:
1736 request['state'] = 'failed'
1737 else:
1738 request['state'] = 'fulfilled'
1739 nodes = []
1740 for node in request['node_types']:
1741 nodeid = self.makeNode(oid, node)
1742 nodes.append(nodeid)
1743 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001744
James E. Blaira38c28e2017-01-04 10:33:20 -08001745 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001746 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001747 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001748 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001749 try:
1750 self.client.set(path, data)
1751 except kazoo.exceptions.NoNodeError:
1752 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001753
1754
James E. Blair498059b2016-12-20 13:50:13 -08001755class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001756 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001757 super(ChrootedKazooFixture, self).__init__()
1758
1759 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1760 if ':' in zk_host:
1761 host, port = zk_host.split(':')
1762 else:
1763 host = zk_host
1764 port = None
1765
1766 self.zookeeper_host = host
1767
1768 if not port:
1769 self.zookeeper_port = 2181
1770 else:
1771 self.zookeeper_port = int(port)
1772
Clark Boylan621ec9a2017-04-07 17:41:33 -07001773 self.test_id = test_id
1774
James E. Blair498059b2016-12-20 13:50:13 -08001775 def _setUp(self):
1776 # Make sure the test chroot paths do not conflict
1777 random_bits = ''.join(random.choice(string.ascii_lowercase +
1778 string.ascii_uppercase)
1779 for x in range(8))
1780
Clark Boylan621ec9a2017-04-07 17:41:33 -07001781 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001782 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1783
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001784 self.addCleanup(self._cleanup)
1785
James E. Blair498059b2016-12-20 13:50:13 -08001786 # Ensure the chroot path exists and clean up any pre-existing znodes.
1787 _tmp_client = kazoo.client.KazooClient(
1788 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1789 _tmp_client.start()
1790
1791 if _tmp_client.exists(self.zookeeper_chroot):
1792 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1793
1794 _tmp_client.ensure_path(self.zookeeper_chroot)
1795 _tmp_client.stop()
1796 _tmp_client.close()
1797
James E. Blair498059b2016-12-20 13:50:13 -08001798 def _cleanup(self):
1799 '''Remove the chroot path.'''
1800 # Need a non-chroot'ed client to remove the chroot path
1801 _tmp_client = kazoo.client.KazooClient(
1802 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1803 _tmp_client.start()
1804 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1805 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001806 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001807
1808
Joshua Heskethd78b4482015-09-14 16:56:34 -06001809class MySQLSchemaFixture(fixtures.Fixture):
1810 def setUp(self):
1811 super(MySQLSchemaFixture, self).setUp()
1812
1813 random_bits = ''.join(random.choice(string.ascii_lowercase +
1814 string.ascii_uppercase)
1815 for x in range(8))
1816 self.name = '%s_%s' % (random_bits, os.getpid())
1817 self.passwd = uuid.uuid4().hex
1818 db = pymysql.connect(host="localhost",
1819 user="openstack_citest",
1820 passwd="openstack_citest",
1821 db="openstack_citest")
1822 cur = db.cursor()
1823 cur.execute("create database %s" % self.name)
1824 cur.execute(
1825 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1826 (self.name, self.name, self.passwd))
1827 cur.execute("flush privileges")
1828
1829 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1830 self.passwd,
1831 self.name)
1832 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1833 self.addCleanup(self.cleanup)
1834
1835 def cleanup(self):
1836 db = pymysql.connect(host="localhost",
1837 user="openstack_citest",
1838 passwd="openstack_citest",
1839 db="openstack_citest")
1840 cur = db.cursor()
1841 cur.execute("drop database %s" % self.name)
1842 cur.execute("drop user '%s'@'localhost'" % self.name)
1843 cur.execute("flush privileges")
1844
1845
Maru Newby3fe5f852015-01-13 04:22:14 +00001846class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001847 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001848 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001849
James E. Blair1c236df2017-02-01 14:07:24 -08001850 def attachLogs(self, *args):
1851 def reader():
1852 self._log_stream.seek(0)
1853 while True:
1854 x = self._log_stream.read(4096)
1855 if not x:
1856 break
1857 yield x.encode('utf8')
1858 content = testtools.content.content_from_reader(
1859 reader,
1860 testtools.content_type.UTF8_TEXT,
1861 False)
1862 self.addDetail('logging', content)
1863
Clark Boylanb640e052014-04-03 16:41:46 -07001864 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001865 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001866 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1867 try:
1868 test_timeout = int(test_timeout)
1869 except ValueError:
1870 # If timeout value is invalid do not set a timeout.
1871 test_timeout = 0
1872 if test_timeout > 0:
1873 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1874
1875 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1876 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1877 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1878 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1879 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1880 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1881 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1882 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1883 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1884 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001885 self._log_stream = StringIO()
1886 self.addOnException(self.attachLogs)
1887 else:
1888 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001889
James E. Blair73b41772017-05-22 13:22:55 -07001890 # NOTE(jeblair): this is temporary extra debugging to try to
1891 # track down a possible leak.
1892 orig_git_repo_init = git.Repo.__init__
1893
1894 def git_repo_init(myself, *args, **kw):
1895 orig_git_repo_init(myself, *args, **kw)
1896 self.log.debug("Created git repo 0x%x %s" %
1897 (id(myself), repr(myself)))
1898
1899 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1900 git_repo_init))
1901
James E. Blair1c236df2017-02-01 14:07:24 -08001902 handler = logging.StreamHandler(self._log_stream)
1903 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1904 '%(levelname)-8s %(message)s')
1905 handler.setFormatter(formatter)
1906
1907 logger = logging.getLogger()
1908 logger.setLevel(logging.DEBUG)
1909 logger.addHandler(handler)
1910
Clark Boylan3410d532017-04-25 12:35:29 -07001911 # Make sure we don't carry old handlers around in process state
1912 # which slows down test runs
1913 self.addCleanup(logger.removeHandler, handler)
1914 self.addCleanup(handler.close)
1915 self.addCleanup(handler.flush)
1916
James E. Blair1c236df2017-02-01 14:07:24 -08001917 # NOTE(notmorgan): Extract logging overrides for specific
1918 # libraries from the OS_LOG_DEFAULTS env and create loggers
1919 # for each. This is used to limit the output during test runs
1920 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001921 log_defaults_from_env = os.environ.get(
1922 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001923 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001924
James E. Blairdce6cea2016-12-20 16:45:32 -08001925 if log_defaults_from_env:
1926 for default in log_defaults_from_env.split(','):
1927 try:
1928 name, level_str = default.split('=', 1)
1929 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001930 logger = logging.getLogger(name)
1931 logger.setLevel(level)
1932 logger.addHandler(handler)
1933 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001934 except ValueError:
1935 # NOTE(notmorgan): Invalid format of the log default,
1936 # skip and don't try and apply a logger for the
1937 # specified module
1938 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001939
Maru Newby3fe5f852015-01-13 04:22:14 +00001940
1941class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001942 """A test case with a functioning Zuul.
1943
1944 The following class variables are used during test setup and can
1945 be overidden by subclasses but are effectively read-only once a
1946 test method starts running:
1947
1948 :cvar str config_file: This points to the main zuul config file
1949 within the fixtures directory. Subclasses may override this
1950 to obtain a different behavior.
1951
1952 :cvar str tenant_config_file: This is the tenant config file
1953 (which specifies from what git repos the configuration should
1954 be loaded). It defaults to the value specified in
1955 `config_file` but can be overidden by subclasses to obtain a
1956 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001957 configuration. See also the :py:func:`simple_layout`
1958 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001959
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001960 :cvar bool create_project_keys: Indicates whether Zuul should
1961 auto-generate keys for each project, or whether the test
1962 infrastructure should insert dummy keys to save time during
1963 startup. Defaults to False.
1964
James E. Blaire7b99a02016-08-05 14:27:34 -07001965 The following are instance variables that are useful within test
1966 methods:
1967
1968 :ivar FakeGerritConnection fake_<connection>:
1969 A :py:class:`~tests.base.FakeGerritConnection` will be
1970 instantiated for each connection present in the config file
1971 and stored here. For instance, `fake_gerrit` will hold the
1972 FakeGerritConnection object for a connection named `gerrit`.
1973
1974 :ivar FakeGearmanServer gearman_server: An instance of
1975 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1976 server that all of the Zuul components in this test use to
1977 communicate with each other.
1978
Paul Belanger174a8272017-03-14 13:20:10 -04001979 :ivar RecordingExecutorServer executor_server: An instance of
1980 :py:class:`~tests.base.RecordingExecutorServer` which is the
1981 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001982
1983 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1984 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001985 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001986 list upon completion.
1987
1988 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1989 objects representing completed builds. They are appended to
1990 the list in the order they complete.
1991
1992 """
1993
James E. Blair83005782015-12-11 14:46:03 -08001994 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001995 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001996 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001997 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001998
1999 def _startMerger(self):
2000 self.merge_server = zuul.merger.server.MergeServer(self.config,
2001 self.connections)
2002 self.merge_server.start()
2003
Maru Newby3fe5f852015-01-13 04:22:14 +00002004 def setUp(self):
2005 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002006
2007 self.setupZK()
2008
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002009 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002010 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002011 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2012 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002013 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002014 tmp_root = tempfile.mkdtemp(
2015 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002016 self.test_root = os.path.join(tmp_root, "zuul-test")
2017 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002018 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002019 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002020 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002021 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2022 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002023
2024 if os.path.exists(self.test_root):
2025 shutil.rmtree(self.test_root)
2026 os.makedirs(self.test_root)
2027 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002028 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002029 os.makedirs(self.merger_state_root)
2030 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002031
2032 # Make per test copy of Configuration.
2033 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002034 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2035 if not os.path.exists(self.private_key_file):
2036 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2037 shutil.copy(src_private_key_file, self.private_key_file)
2038 shutil.copy('{}.pub'.format(src_private_key_file),
2039 '{}.pub'.format(self.private_key_file))
2040 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002041 self.config.set('scheduler', 'tenant_config',
2042 os.path.join(
2043 FIXTURE_DIR,
2044 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002045 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002046 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002047 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002048 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002049 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002050
Clark Boylanb640e052014-04-03 16:41:46 -07002051 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07002052 if self.config.has_section('statsd'):
2053 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002054 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002055
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002056 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002057
2058 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002059 self.log.info("Gearman server on port %s" %
2060 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002061 if self.use_ssl:
2062 self.log.info('SSL enabled for gearman')
2063 self.config.set(
2064 'gearman', 'ssl_ca',
2065 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2066 self.config.set(
2067 'gearman', 'ssl_cert',
2068 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2069 self.config.set(
2070 'gearman', 'ssl_key',
2071 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002072
James E. Blaire511d2f2016-12-08 15:22:26 -08002073 gerritsource.GerritSource.replication_timeout = 1.5
2074 gerritsource.GerritSource.replication_retry_interval = 0.5
2075 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002076
Joshua Hesketh352264b2015-08-11 23:42:08 +10002077 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002078 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002079
Jan Hruban7083edd2015-08-21 14:00:54 +02002080 self.webapp = zuul.webapp.WebApp(
2081 self.sched, port=0, listen_address='127.0.0.1')
2082
Jan Hruban6b71aff2015-10-22 16:58:08 +02002083 self.event_queues = [
2084 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002085 self.sched.trigger_event_queue,
2086 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002087 ]
2088
James E. Blairfef78942016-03-11 16:28:56 -08002089 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002090 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002091
Paul Belanger174a8272017-03-14 13:20:10 -04002092 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002093 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002094 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002095 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002096 _test_root=self.test_root,
2097 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002098 self.executor_server.start()
2099 self.history = self.executor_server.build_history
2100 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002101
Paul Belanger174a8272017-03-14 13:20:10 -04002102 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002103 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002104 self.merge_client = zuul.merger.client.MergeClient(
2105 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002106 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002107 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002108 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002109
James E. Blair0d5a36e2017-02-21 10:53:44 -05002110 self.fake_nodepool = FakeNodepool(
2111 self.zk_chroot_fixture.zookeeper_host,
2112 self.zk_chroot_fixture.zookeeper_port,
2113 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002114
Paul Belanger174a8272017-03-14 13:20:10 -04002115 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002116 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002117 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002118 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002119
Clark Boylanb640e052014-04-03 16:41:46 -07002120 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002121 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002122 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002123 # Cleanups are run in reverse order
2124 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002125 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002126 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002127
James E. Blairb9c0d772017-03-03 14:34:49 -08002128 self.sched.reconfigure(self.config)
2129 self.sched.resume()
2130
Tobias Henkel7df274b2017-05-26 17:41:11 +02002131 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002132 # Set up gerrit related fakes
2133 # Set a changes database so multiple FakeGerrit's can report back to
2134 # a virtual canonical database given by the configured hostname
2135 self.gerrit_changes_dbs = {}
2136
2137 def getGerritConnection(driver, name, config):
2138 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2139 con = FakeGerritConnection(driver, name, config,
2140 changes_db=db,
2141 upstream_root=self.upstream_root)
2142 self.event_queues.append(con.event_queue)
2143 setattr(self, 'fake_' + name, con)
2144 return con
2145
2146 self.useFixture(fixtures.MonkeyPatch(
2147 'zuul.driver.gerrit.GerritDriver.getConnection',
2148 getGerritConnection))
2149
Gregory Haynes4fc12542015-04-22 20:38:06 -07002150 def getGithubConnection(driver, name, config):
2151 con = FakeGithubConnection(driver, name, config,
2152 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002153 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002154 setattr(self, 'fake_' + name, con)
2155 return con
2156
2157 self.useFixture(fixtures.MonkeyPatch(
2158 'zuul.driver.github.GithubDriver.getConnection',
2159 getGithubConnection))
2160
James E. Blaire511d2f2016-12-08 15:22:26 -08002161 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002162 # TODO(jhesketh): This should come from lib.connections for better
2163 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002164 # Register connections from the config
2165 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002166
Joshua Hesketh352264b2015-08-11 23:42:08 +10002167 def FakeSMTPFactory(*args, **kw):
2168 args = [self.smtp_messages] + list(args)
2169 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002170
Joshua Hesketh352264b2015-08-11 23:42:08 +10002171 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002172
James E. Blaire511d2f2016-12-08 15:22:26 -08002173 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002174 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002175 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002176
James E. Blair83005782015-12-11 14:46:03 -08002177 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002178 # This creates the per-test configuration object. It can be
2179 # overriden by subclasses, but should not need to be since it
2180 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002181 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002182 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002183
James E. Blair39840362017-06-23 20:34:02 +01002184 sections = ['zuul', 'scheduler', 'executor', 'merger']
2185 for section in sections:
2186 if not self.config.has_section(section):
2187 self.config.add_section(section)
2188
James E. Blair06cc3922017-04-19 10:08:10 -07002189 if not self.setupSimpleLayout():
2190 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002191 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002192 self.tenant_config_file)
2193 git_path = os.path.join(
2194 os.path.dirname(
2195 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2196 'git')
2197 if os.path.exists(git_path):
2198 for reponame in os.listdir(git_path):
2199 project = reponame.replace('_', '/')
2200 self.copyDirToRepo(project,
2201 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002202 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002203 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002204 self.setupAllProjectKeys()
2205
James E. Blair06cc3922017-04-19 10:08:10 -07002206 def setupSimpleLayout(self):
2207 # If the test method has been decorated with a simple_layout,
2208 # use that instead of the class tenant_config_file. Set up a
2209 # single config-project with the specified layout, and
2210 # initialize repos for all of the 'project' entries which
2211 # appear in the layout.
2212 test_name = self.id().split('.')[-1]
2213 test = getattr(self, test_name)
2214 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002215 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002216 else:
2217 return False
2218
James E. Blairb70e55a2017-04-19 12:57:02 -07002219 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002220 path = os.path.join(FIXTURE_DIR, path)
2221 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002222 data = f.read()
2223 layout = yaml.safe_load(data)
2224 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002225 untrusted_projects = []
2226 for item in layout:
2227 if 'project' in item:
2228 name = item['project']['name']
2229 untrusted_projects.append(name)
2230 self.init_repo(name)
2231 self.addCommitToRepo(name, 'initial commit',
2232 files={'README': ''},
2233 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002234 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002235 if 'run' in item['job']:
2236 files['%s.yaml' % item['job']['run']] = ''
2237 for fn in zuul.configloader.as_list(
2238 item['job'].get('pre-run', [])):
2239 files['%s.yaml' % fn] = ''
2240 for fn in zuul.configloader.as_list(
2241 item['job'].get('post-run', [])):
2242 files['%s.yaml' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002243
2244 root = os.path.join(self.test_root, "config")
2245 if not os.path.exists(root):
2246 os.makedirs(root)
2247 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2248 config = [{'tenant':
2249 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002250 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002251 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002252 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002253 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002254 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002255 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002256 os.path.join(FIXTURE_DIR, f.name))
2257
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002258 self.init_repo('org/common-config')
2259 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002260 files, branch='master', tag='init')
2261
2262 return True
2263
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002264 def setupAllProjectKeys(self):
2265 if self.create_project_keys:
2266 return
2267
James E. Blair39840362017-06-23 20:34:02 +01002268 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002269 with open(os.path.join(FIXTURE_DIR, path)) as f:
2270 tenant_config = yaml.safe_load(f.read())
2271 for tenant in tenant_config:
2272 sources = tenant['tenant']['source']
2273 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002274 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002275 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002276 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002277 self.setupProjectKeys(source, project)
2278
2279 def setupProjectKeys(self, source, project):
2280 # Make sure we set up an RSA key for the project so that we
2281 # don't spend time generating one:
2282
James E. Blair6459db12017-06-29 14:57:20 -07002283 if isinstance(project, dict):
2284 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002285 key_root = os.path.join(self.state_root, 'keys')
2286 if not os.path.isdir(key_root):
2287 os.mkdir(key_root, 0o700)
2288 private_key_file = os.path.join(key_root, source, project + '.pem')
2289 private_key_dir = os.path.dirname(private_key_file)
2290 self.log.debug("Installing test keys for project %s at %s" % (
2291 project, private_key_file))
2292 if not os.path.isdir(private_key_dir):
2293 os.makedirs(private_key_dir)
2294 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2295 with open(private_key_file, 'w') as o:
2296 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002297
James E. Blair498059b2016-12-20 13:50:13 -08002298 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002299 self.zk_chroot_fixture = self.useFixture(
2300 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002301 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002302 self.zk_chroot_fixture.zookeeper_host,
2303 self.zk_chroot_fixture.zookeeper_port,
2304 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002305
James E. Blair96c6bf82016-01-15 16:20:40 -08002306 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002307 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002308
2309 files = {}
2310 for (dirpath, dirnames, filenames) in os.walk(source_path):
2311 for filename in filenames:
2312 test_tree_filepath = os.path.join(dirpath, filename)
2313 common_path = os.path.commonprefix([test_tree_filepath,
2314 source_path])
2315 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2316 with open(test_tree_filepath, 'r') as f:
2317 content = f.read()
2318 files[relative_filepath] = content
2319 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002320 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002321
James E. Blaire18d4602017-01-05 11:17:28 -08002322 def assertNodepoolState(self):
2323 # Make sure that there are no pending requests
2324
2325 requests = self.fake_nodepool.getNodeRequests()
2326 self.assertEqual(len(requests), 0)
2327
2328 nodes = self.fake_nodepool.getNodes()
2329 for node in nodes:
2330 self.assertFalse(node['_lock'], "Node %s is locked" %
2331 (node['_oid'],))
2332
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002333 def assertNoGeneratedKeys(self):
2334 # Make sure that Zuul did not generate any project keys
2335 # (unless it was supposed to).
2336
2337 if self.create_project_keys:
2338 return
2339
2340 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2341 test_key = i.read()
2342
2343 key_root = os.path.join(self.state_root, 'keys')
2344 for root, dirname, files in os.walk(key_root):
2345 for fn in files:
2346 with open(os.path.join(root, fn)) as f:
2347 self.assertEqual(test_key, f.read())
2348
Clark Boylanb640e052014-04-03 16:41:46 -07002349 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002350 self.log.debug("Assert final state")
2351 # Make sure no jobs are running
2352 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002353 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002354 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002355 gc.collect()
2356 for obj in gc.get_objects():
2357 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002358 self.log.debug("Leaked git repo object: 0x%x %s" %
2359 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002360 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002361 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002362 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002363 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002364 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002365 for tenant in self.sched.abide.tenants.values():
2366 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002367 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002368 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002369
2370 def shutdown(self):
2371 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002372 self.executor_server.hold_jobs_in_build = False
2373 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002374 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002375 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002376 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002377 self.sched.stop()
2378 self.sched.join()
2379 self.statsd.stop()
2380 self.statsd.join()
2381 self.webapp.stop()
2382 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002383 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002384 self.fake_nodepool.stop()
2385 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002386 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002387 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002388 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002389 # Further the pydevd threads also need to be whitelisted so debugging
2390 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002391 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002392 'pydevd.CommandThread',
2393 'pydevd.Reader',
2394 'pydevd.Writer',
David Shrewsbury8d06afd2017-11-02 16:58:54 -04002395 'FingerStreamer',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002396 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002397 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002398 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002399 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002400 log_str = ""
2401 for thread_id, stack_frame in sys._current_frames().items():
2402 log_str += "Thread: %s\n" % thread_id
2403 log_str += "".join(traceback.format_stack(stack_frame))
2404 self.log.debug(log_str)
2405 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002406
James E. Blaira002b032017-04-18 10:35:48 -07002407 def assertCleanShutdown(self):
2408 pass
2409
James E. Blairc4ba97a2017-04-19 16:26:24 -07002410 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002411 parts = project.split('/')
2412 path = os.path.join(self.upstream_root, *parts[:-1])
2413 if not os.path.exists(path):
2414 os.makedirs(path)
2415 path = os.path.join(self.upstream_root, project)
2416 repo = git.Repo.init(path)
2417
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002418 with repo.config_writer() as config_writer:
2419 config_writer.set_value('user', 'email', 'user@example.com')
2420 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002421
Clark Boylanb640e052014-04-03 16:41:46 -07002422 repo.index.commit('initial commit')
2423 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002424 if tag:
2425 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002426
James E. Blair97d902e2014-08-21 13:25:56 -07002427 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002428 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002429 repo.git.clean('-x', '-f', '-d')
2430
James E. Blair97d902e2014-08-21 13:25:56 -07002431 def create_branch(self, project, branch):
2432 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002433 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002434 fn = os.path.join(path, 'README')
2435
2436 branch_head = repo.create_head(branch)
2437 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002438 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002439 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002440 f.close()
2441 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002442 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002443
James E. Blair97d902e2014-08-21 13:25:56 -07002444 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002445 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002446 repo.git.clean('-x', '-f', '-d')
2447
Sachi King9f16d522016-03-16 12:20:45 +11002448 def create_commit(self, project):
2449 path = os.path.join(self.upstream_root, project)
2450 repo = git.Repo(path)
2451 repo.head.reference = repo.heads['master']
2452 file_name = os.path.join(path, 'README')
2453 with open(file_name, 'a') as f:
2454 f.write('creating fake commit\n')
2455 repo.index.add([file_name])
2456 commit = repo.index.commit('Creating a fake commit')
2457 return commit.hexsha
2458
James E. Blairf4a5f022017-04-18 14:01:10 -07002459 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002460 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002461 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002462 while len(self.builds):
2463 self.release(self.builds[0])
2464 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002465 i += 1
2466 if count is not None and i >= count:
2467 break
James E. Blairb8c16472015-05-05 14:55:26 -07002468
James E. Blairdf25ddc2017-07-08 07:57:09 -07002469 def getSortedBuilds(self):
2470 "Return the list of currently running builds sorted by name"
2471
2472 return sorted(self.builds, key=lambda x: x.name)
2473
Clark Boylanb640e052014-04-03 16:41:46 -07002474 def release(self, job):
2475 if isinstance(job, FakeBuild):
2476 job.release()
2477 else:
2478 job.waiting = False
2479 self.log.debug("Queued job %s released" % job.unique)
2480 self.gearman_server.wakeConnections()
2481
2482 def getParameter(self, job, name):
2483 if isinstance(job, FakeBuild):
2484 return job.parameters[name]
2485 else:
2486 parameters = json.loads(job.arguments)
2487 return parameters[name]
2488
Clark Boylanb640e052014-04-03 16:41:46 -07002489 def haveAllBuildsReported(self):
2490 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002491 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002492 return False
2493 # Find out if every build that the worker has completed has been
2494 # reported back to Zuul. If it hasn't then that means a Gearman
2495 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002496 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002497 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002498 if not zbuild:
2499 # It has already been reported
2500 continue
2501 # It hasn't been reported yet.
2502 return False
2503 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002504 worker = self.executor_server.executor_worker
2505 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002506 if connection.state == 'GRAB_WAIT':
2507 return False
2508 return True
2509
2510 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002511 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002512 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002513 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002514 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002515 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002516 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002517 for j in conn.related_jobs.values():
2518 if j.unique == build.uuid:
2519 client_job = j
2520 break
2521 if not client_job:
2522 self.log.debug("%s is not known to the gearman client" %
2523 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002524 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002525 if not client_job.handle:
2526 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002527 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002528 server_job = self.gearman_server.jobs.get(client_job.handle)
2529 if not server_job:
2530 self.log.debug("%s is not known to the gearman server" %
2531 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002532 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002533 if not hasattr(server_job, 'waiting'):
2534 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002535 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002536 if server_job.waiting:
2537 continue
James E. Blair17302972016-08-10 16:11:42 -07002538 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002539 self.log.debug("%s has not reported start" % build)
2540 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002541 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002542 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002543 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002544 if worker_build:
2545 if worker_build.isWaiting():
2546 continue
2547 else:
2548 self.log.debug("%s is running" % worker_build)
2549 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002550 else:
James E. Blair962220f2016-08-03 11:22:38 -07002551 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002552 return False
James E. Blaira002b032017-04-18 10:35:48 -07002553 for (build_uuid, job_worker) in \
2554 self.executor_server.job_workers.items():
2555 if build_uuid not in seen_builds:
2556 self.log.debug("%s is not finalized" % build_uuid)
2557 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002558 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002559
James E. Blairdce6cea2016-12-20 16:45:32 -08002560 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002561 if self.fake_nodepool.paused:
2562 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002563 if self.sched.nodepool.requests:
2564 return False
2565 return True
2566
James E. Blaira615c362017-10-02 17:34:42 -07002567 def areAllMergeJobsWaiting(self):
2568 for client_job in list(self.merge_client.jobs):
2569 if not client_job.handle:
2570 self.log.debug("%s has no handle" % client_job)
2571 return False
2572 server_job = self.gearman_server.jobs.get(client_job.handle)
2573 if not server_job:
2574 self.log.debug("%s is not known to the gearman server" %
2575 client_job)
2576 return False
2577 if not hasattr(server_job, 'waiting'):
2578 self.log.debug("%s is being enqueued" % server_job)
2579 return False
2580 if server_job.waiting:
2581 self.log.debug("%s is waiting" % server_job)
2582 continue
2583 self.log.debug("%s is not waiting" % server_job)
2584 return False
2585 return True
2586
Jan Hruban6b71aff2015-10-22 16:58:08 +02002587 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002588 for event_queue in self.event_queues:
2589 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002590
2591 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002592 for event_queue in self.event_queues:
2593 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002594
Clark Boylanb640e052014-04-03 16:41:46 -07002595 def waitUntilSettled(self):
2596 self.log.debug("Waiting until settled...")
2597 start = time.time()
2598 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002599 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002600 self.log.error("Timeout waiting for Zuul to settle")
2601 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002602 for event_queue in self.event_queues:
2603 self.log.error(" %s: %s" %
2604 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002605 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002606 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002607 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002608 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002609 self.log.error("All requests completed: %s" %
2610 (self.areAllNodeRequestsComplete(),))
2611 self.log.error("Merge client jobs: %s" %
2612 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002613 raise Exception("Timeout waiting for Zuul to settle")
2614 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002615
Paul Belanger174a8272017-03-14 13:20:10 -04002616 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002617 # have all build states propogated to zuul?
2618 if self.haveAllBuildsReported():
2619 # Join ensures that the queue is empty _and_ events have been
2620 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002621 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002622 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002623 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002624 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002625 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002626 self.areAllNodeRequestsComplete() and
2627 all(self.eventQueuesEmpty())):
2628 # The queue empty check is placed at the end to
2629 # ensure that if a component adds an event between
2630 # when locked the run handler and checked that the
2631 # components were stable, we don't erroneously
2632 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002633 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002634 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002635 self.log.debug("...settled.")
2636 return
2637 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002638 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002639 self.sched.wake_event.wait(0.1)
2640
2641 def countJobResults(self, jobs, result):
2642 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002643 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002644
Monty Taylor0d926122017-05-24 08:07:56 -05002645 def getBuildByName(self, name):
2646 for build in self.builds:
2647 if build.name == name:
2648 return build
2649 raise Exception("Unable to find build %s" % name)
2650
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002651 def assertJobNotInHistory(self, name, project=None):
2652 for job in self.history:
2653 if (project is None or
2654 job.parameters['zuul']['project']['name'] == project):
2655 self.assertNotEqual(job.name, name,
2656 'Job %s found in history' % name)
2657
James E. Blair96c6bf82016-01-15 16:20:40 -08002658 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002659 for job in self.history:
2660 if (job.name == name and
2661 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002662 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002663 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002664 raise Exception("Unable to find job %s in history" % name)
2665
2666 def assertEmptyQueues(self):
2667 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002668 for tenant in self.sched.abide.tenants.values():
2669 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002670 for pipeline_queue in pipeline.queues:
2671 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002672 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002673 pipeline.name, pipeline_queue.name,
2674 pipeline_queue.queue))
2675 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002676 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002677
2678 def assertReportedStat(self, key, value=None, kind=None):
2679 start = time.time()
2680 while time.time() < (start + 5):
2681 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002682 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002683 if key == k:
2684 if value is None and kind is None:
2685 return
2686 elif value:
2687 if value == v:
2688 return
2689 elif kind:
2690 if v.endswith('|' + kind):
2691 return
2692 time.sleep(0.1)
2693
Clark Boylanb640e052014-04-03 16:41:46 -07002694 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002695
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002696 def assertBuilds(self, builds):
2697 """Assert that the running builds are as described.
2698
2699 The list of running builds is examined and must match exactly
2700 the list of builds described by the input.
2701
2702 :arg list builds: A list of dictionaries. Each item in the
2703 list must match the corresponding build in the build
2704 history, and each element of the dictionary must match the
2705 corresponding attribute of the build.
2706
2707 """
James E. Blair3158e282016-08-19 09:34:11 -07002708 try:
2709 self.assertEqual(len(self.builds), len(builds))
2710 for i, d in enumerate(builds):
2711 for k, v in d.items():
2712 self.assertEqual(
2713 getattr(self.builds[i], k), v,
2714 "Element %i in builds does not match" % (i,))
2715 except Exception:
2716 for build in self.builds:
2717 self.log.error("Running build: %s" % build)
2718 else:
2719 self.log.error("No running builds")
2720 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002721
James E. Blairb536ecc2016-08-31 10:11:42 -07002722 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002723 """Assert that the completed builds are as described.
2724
2725 The list of completed builds is examined and must match
2726 exactly the list of builds described by the input.
2727
2728 :arg list history: A list of dictionaries. Each item in the
2729 list must match the corresponding build in the build
2730 history, and each element of the dictionary must match the
2731 corresponding attribute of the build.
2732
James E. Blairb536ecc2016-08-31 10:11:42 -07002733 :arg bool ordered: If true, the history must match the order
2734 supplied, if false, the builds are permitted to have
2735 arrived in any order.
2736
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002737 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002738 def matches(history_item, item):
2739 for k, v in item.items():
2740 if getattr(history_item, k) != v:
2741 return False
2742 return True
James E. Blair3158e282016-08-19 09:34:11 -07002743 try:
2744 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002745 if ordered:
2746 for i, d in enumerate(history):
2747 if not matches(self.history[i], d):
2748 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002749 "Element %i in history does not match %s" %
2750 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002751 else:
2752 unseen = self.history[:]
2753 for i, d in enumerate(history):
2754 found = False
2755 for unseen_item in unseen:
2756 if matches(unseen_item, d):
2757 found = True
2758 unseen.remove(unseen_item)
2759 break
2760 if not found:
2761 raise Exception("No match found for element %i "
2762 "in history" % (i,))
2763 if unseen:
2764 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002765 except Exception:
2766 for build in self.history:
2767 self.log.error("Completed build: %s" % build)
2768 else:
2769 self.log.error("No completed builds")
2770 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002771
James E. Blair6ac368c2016-12-22 18:07:20 -08002772 def printHistory(self):
2773 """Log the build history.
2774
2775 This can be useful during tests to summarize what jobs have
2776 completed.
2777
2778 """
2779 self.log.debug("Build history:")
2780 for build in self.history:
2781 self.log.debug(build)
2782
James E. Blair59fdbac2015-12-07 17:08:06 -08002783 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002784 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2785
James E. Blair9ea70072017-04-19 16:05:30 -07002786 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002787 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002788 if not os.path.exists(root):
2789 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002790 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2791 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002792- tenant:
2793 name: openstack
2794 source:
2795 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002796 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002797 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002798 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002799 - org/project
2800 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002801 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002802 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002803 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002804 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002805 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002806
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002807 def addCommitToRepo(self, project, message, files,
2808 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002809 path = os.path.join(self.upstream_root, project)
2810 repo = git.Repo(path)
2811 repo.head.reference = branch
2812 zuul.merger.merger.reset_repo_to_head(repo)
2813 for fn, content in files.items():
2814 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002815 try:
2816 os.makedirs(os.path.dirname(fn))
2817 except OSError:
2818 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002819 with open(fn, 'w') as f:
2820 f.write(content)
2821 repo.index.add([fn])
2822 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002823 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002824 repo.heads[branch].commit = commit
2825 repo.head.reference = branch
2826 repo.git.clean('-x', '-f', '-d')
2827 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002828 if tag:
2829 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002830 return before
2831
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002832 def commitConfigUpdate(self, project_name, source_name):
2833 """Commit an update to zuul.yaml
2834
2835 This overwrites the zuul.yaml in the specificed project with
2836 the contents specified.
2837
2838 :arg str project_name: The name of the project containing
2839 zuul.yaml (e.g., common-config)
2840
2841 :arg str source_name: The path to the file (underneath the
2842 test fixture directory) whose contents should be used to
2843 replace zuul.yaml.
2844 """
2845
2846 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002847 files = {}
2848 with open(source_path, 'r') as f:
2849 data = f.read()
2850 layout = yaml.safe_load(data)
2851 files['zuul.yaml'] = data
2852 for item in layout:
2853 if 'job' in item:
2854 jobname = item['job']['name']
2855 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002856 before = self.addCommitToRepo(
2857 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002858 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002859 return before
2860
Clint Byrum627ba362017-08-14 13:20:40 -07002861 def newTenantConfig(self, source_name):
2862 """ Use this to update the tenant config file in tests
2863
2864 This will update self.tenant_config_file to point to a temporary file
2865 for the duration of this particular test. The content of that file will
2866 be taken from FIXTURE_DIR/source_name
2867
2868 After the test the original value of self.tenant_config_file will be
2869 restored.
2870
2871 :arg str source_name: The path of the file under
2872 FIXTURE_DIR that will be used to populate the new tenant
2873 config file.
2874 """
2875 source_path = os.path.join(FIXTURE_DIR, source_name)
2876 orig_tenant_config_file = self.tenant_config_file
2877 with tempfile.NamedTemporaryFile(
2878 delete=False, mode='wb') as new_tenant_config:
2879 self.tenant_config_file = new_tenant_config.name
2880 with open(source_path, mode='rb') as source_tenant_config:
2881 new_tenant_config.write(source_tenant_config.read())
2882 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2883 self.setupAllProjectKeys()
2884 self.log.debug(
2885 'tenant_config_file = {}'.format(self.tenant_config_file))
2886
2887 def _restoreTenantConfig():
2888 self.log.debug(
2889 'restoring tenant_config_file = {}'.format(
2890 orig_tenant_config_file))
2891 os.unlink(self.tenant_config_file)
2892 self.tenant_config_file = orig_tenant_config_file
2893 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2894 self.addCleanup(_restoreTenantConfig)
2895
James E. Blair7fc8daa2016-08-08 15:37:15 -07002896 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002897
James E. Blair7fc8daa2016-08-08 15:37:15 -07002898 """Inject a Fake (Gerrit) event.
2899
2900 This method accepts a JSON-encoded event and simulates Zuul
2901 having received it from Gerrit. It could (and should)
2902 eventually apply to any connection type, but is currently only
2903 used with Gerrit connections. The name of the connection is
2904 used to look up the corresponding server, and the event is
2905 simulated as having been received by all Zuul connections
2906 attached to that server. So if two Gerrit connections in Zuul
2907 are connected to the same Gerrit server, and you invoke this
2908 method specifying the name of one of them, the event will be
2909 received by both.
2910
2911 .. note::
2912
2913 "self.fake_gerrit.addEvent" calls should be migrated to
2914 this method.
2915
2916 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002917 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002918 :arg str event: The JSON-encoded event.
2919
2920 """
2921 specified_conn = self.connections.connections[connection]
2922 for conn in self.connections.connections.values():
2923 if (isinstance(conn, specified_conn.__class__) and
2924 specified_conn.server == conn.server):
2925 conn.addEvent(event)
2926
James E. Blaird8af5422017-05-24 13:59:40 -07002927 def getUpstreamRepos(self, projects):
2928 """Return upstream git repo objects for the listed projects
2929
2930 :arg list projects: A list of strings, each the canonical name
2931 of a project.
2932
2933 :returns: A dictionary of {name: repo} for every listed
2934 project.
2935 :rtype: dict
2936
2937 """
2938
2939 repos = {}
2940 for project in projects:
2941 # FIXME(jeblair): the upstream root does not yet have a
2942 # hostname component; that needs to be added, and this
2943 # line removed:
2944 tmp_project_name = '/'.join(project.split('/')[1:])
2945 path = os.path.join(self.upstream_root, tmp_project_name)
2946 repo = git.Repo(path)
2947 repos[project] = repo
2948 return repos
2949
James E. Blair3f876d52016-07-22 13:07:14 -07002950
2951class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002952 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002953 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002954
Jamie Lennox7655b552017-03-17 12:33:38 +11002955 @contextmanager
2956 def jobLog(self, build):
2957 """Print job logs on assertion errors
2958
2959 This method is a context manager which, if it encounters an
2960 ecxeption, adds the build log to the debug output.
2961
2962 :arg Build build: The build that's being asserted.
2963 """
2964 try:
2965 yield
2966 except Exception:
2967 path = os.path.join(self.test_root, build.uuid,
2968 'work', 'logs', 'job-output.txt')
2969 with open(path) as f:
2970 self.log.debug(f.read())
2971 raise
2972
Joshua Heskethd78b4482015-09-14 16:56:34 -06002973
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002974class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002975 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002976 use_ssl = True
2977
2978
Joshua Heskethd78b4482015-09-14 16:56:34 -06002979class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002980 def setup_config(self):
2981 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002982 for section_name in self.config.sections():
2983 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2984 section_name, re.I)
2985 if not con_match:
2986 continue
2987
2988 if self.config.get(section_name, 'driver') == 'sql':
2989 f = MySQLSchemaFixture()
2990 self.useFixture(f)
2991 if (self.config.get(section_name, 'dburi') ==
2992 '$MYSQL_FIXTURE_DBURI$'):
2993 self.config.set(section_name, 'dburi', f.dburi)