blob: d3c5d62540f43ceeb8fb823396ae34525802c836 [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
Jan Hruban49bff072015-11-03 11:45:46 +010070from zuul.exceptions import MergeFailure
Clark Boylanb640e052014-04-03 16:41:46 -070071
72FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
73 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080074
75KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070076
Clark Boylanb640e052014-04-03 16:41:46 -070077
78def repack_repo(path):
79 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
80 output = subprocess.Popen(cmd, close_fds=True,
81 stdout=subprocess.PIPE,
82 stderr=subprocess.PIPE)
83 out = output.communicate()
84 if output.returncode:
85 raise Exception("git repack returned %d" % output.returncode)
86 return out
87
88
89def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040090 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070091
92
James E. Blaira190f3b2015-01-05 14:56:54 -080093def iterate_timeout(max_seconds, purpose):
94 start = time.time()
95 count = 0
96 while (time.time() < start + max_seconds):
97 count += 1
98 yield count
99 time.sleep(0)
100 raise Exception("Timeout waiting for %s" % purpose)
101
102
Jesse Keating436a5452017-04-20 11:48:41 -0700103def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700104 """Specify a layout file for use by a test method.
105
106 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700107 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700108
109 Some tests require only a very simple configuration. For those,
110 establishing a complete config directory hierachy is too much
111 work. In those cases, you can add a simple zuul.yaml file to the
112 test fixtures directory (in fixtures/layouts/foo.yaml) and use
113 this decorator to indicate the test method should use that rather
114 than the tenant config file specified by the test class.
115
116 The decorator will cause that layout file to be added to a
117 config-project called "common-config" and each "project" instance
118 referenced in the layout file will have a git repo automatically
119 initialized.
120 """
121
122 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700123 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700124 return test
125 return decorator
126
127
Gregory Haynes4fc12542015-04-22 20:38:06 -0700128class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700129 _common_path_default = "refs/changes"
130 _points_to_commits_only = True
131
132
Gregory Haynes4fc12542015-04-22 20:38:06 -0700133class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200134 categories = {'Approved': ('Approved', -1, 1),
135 'Code-Review': ('Code-Review', -2, 2),
136 'Verified': ('Verified', -2, 2)}
137
Clark Boylanb640e052014-04-03 16:41:46 -0700138 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700139 status='NEW', upstream_root=None, files={},
140 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700141 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700142 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700143 self.reported = 0
144 self.queried = 0
145 self.patchsets = []
146 self.number = number
147 self.project = project
148 self.branch = branch
149 self.subject = subject
150 self.latest_patchset = 0
151 self.depends_on_change = None
152 self.needed_by_changes = []
153 self.fail_merge = False
154 self.messages = []
155 self.data = {
156 'branch': branch,
157 'comments': [],
158 'commitMessage': subject,
159 'createdOn': time.time(),
160 'id': 'I' + random_sha1(),
161 'lastUpdated': time.time(),
162 'number': str(number),
163 'open': status == 'NEW',
164 'owner': {'email': 'user@example.com',
165 'name': 'User Name',
166 'username': 'username'},
167 'patchSets': self.patchsets,
168 'project': project,
169 'status': status,
170 'subject': subject,
171 'submitRecords': [],
172 'url': 'https://hostname/%s' % number}
173
174 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700175 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700176 self.data['submitRecords'] = self.getSubmitRecords()
177 self.open = status == 'NEW'
178
James E. Blair289f5932017-07-27 15:02:29 -0700179 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700180 path = os.path.join(self.upstream_root, self.project)
181 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700182 if parent is None:
183 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700184 ref = GerritChangeReference.create(
185 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700186 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700187 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700188 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700189 repo.git.clean('-x', '-f', '-d')
190
191 path = os.path.join(self.upstream_root, self.project)
192 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700193 for fn, content in files.items():
194 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700195 if content is None:
196 os.unlink(fn)
197 repo.index.remove([fn])
198 else:
199 d = os.path.dirname(fn)
200 if not os.path.exists(d):
201 os.makedirs(d)
202 with open(fn, 'w') as f:
203 f.write(content)
204 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700205 else:
206 for fni in range(100):
207 fn = os.path.join(path, str(fni))
208 f = open(fn, 'w')
209 for ci in range(4096):
210 f.write(random.choice(string.printable))
211 f.close()
212 repo.index.add([fn])
213
214 r = repo.index.commit(msg)
215 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700216 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700217 repo.git.clean('-x', '-f', '-d')
218 repo.heads['master'].checkout()
219 return r
220
James E. Blair289f5932017-07-27 15:02:29 -0700221 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700222 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700223 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700224 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700225 data = ("test %s %s %s\n" %
226 (self.branch, self.number, self.latest_patchset))
227 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700228 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700229 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700230 ps_files = [{'file': '/COMMIT_MSG',
231 'type': 'ADDED'},
232 {'file': 'README',
233 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700234 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700235 ps_files.append({'file': f, 'type': 'ADDED'})
236 d = {'approvals': [],
237 'createdOn': time.time(),
238 'files': ps_files,
239 'number': str(self.latest_patchset),
240 'ref': 'refs/changes/1/%s/%s' % (self.number,
241 self.latest_patchset),
242 'revision': c.hexsha,
243 'uploader': {'email': 'user@example.com',
244 'name': 'User name',
245 'username': 'user'}}
246 self.data['currentPatchSet'] = d
247 self.patchsets.append(d)
248 self.data['submitRecords'] = self.getSubmitRecords()
249
250 def getPatchsetCreatedEvent(self, patchset):
251 event = {"type": "patchset-created",
252 "change": {"project": self.project,
253 "branch": self.branch,
254 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
255 "number": str(self.number),
256 "subject": self.subject,
257 "owner": {"name": "User Name"},
258 "url": "https://hostname/3"},
259 "patchSet": self.patchsets[patchset - 1],
260 "uploader": {"name": "User Name"}}
261 return event
262
263 def getChangeRestoredEvent(self):
264 event = {"type": "change-restored",
265 "change": {"project": self.project,
266 "branch": self.branch,
267 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
268 "number": str(self.number),
269 "subject": self.subject,
270 "owner": {"name": "User Name"},
271 "url": "https://hostname/3"},
272 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100273 "patchSet": self.patchsets[-1],
274 "reason": ""}
275 return event
276
277 def getChangeAbandonedEvent(self):
278 event = {"type": "change-abandoned",
279 "change": {"project": self.project,
280 "branch": self.branch,
281 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
282 "number": str(self.number),
283 "subject": self.subject,
284 "owner": {"name": "User Name"},
285 "url": "https://hostname/3"},
286 "abandoner": {"name": "User Name"},
287 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700288 "reason": ""}
289 return event
290
291 def getChangeCommentEvent(self, patchset):
292 event = {"type": "comment-added",
293 "change": {"project": self.project,
294 "branch": self.branch,
295 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
296 "number": str(self.number),
297 "subject": self.subject,
298 "owner": {"name": "User Name"},
299 "url": "https://hostname/3"},
300 "patchSet": self.patchsets[patchset - 1],
301 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200302 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700303 "description": "Code-Review",
304 "value": "0"}],
305 "comment": "This is a comment"}
306 return event
307
James E. Blairc2a5ed72017-02-20 14:12:01 -0500308 def getChangeMergedEvent(self):
309 event = {"submitter": {"name": "Jenkins",
310 "username": "jenkins"},
311 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
312 "patchSet": self.patchsets[-1],
313 "change": self.data,
314 "type": "change-merged",
315 "eventCreatedOn": 1487613810}
316 return event
317
James E. Blair8cce42e2016-10-18 08:18:36 -0700318 def getRefUpdatedEvent(self):
319 path = os.path.join(self.upstream_root, self.project)
320 repo = git.Repo(path)
321 oldrev = repo.heads[self.branch].commit.hexsha
322
323 event = {
324 "type": "ref-updated",
325 "submitter": {
326 "name": "User Name",
327 },
328 "refUpdate": {
329 "oldRev": oldrev,
330 "newRev": self.patchsets[-1]['revision'],
331 "refName": self.branch,
332 "project": self.project,
333 }
334 }
335 return event
336
Joshua Hesketh642824b2014-07-01 17:54:59 +1000337 def addApproval(self, category, value, username='reviewer_john',
338 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700339 if not granted_on:
340 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000341 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200342 'description': self.categories[category][0],
343 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000344 'value': str(value),
345 'by': {
346 'username': username,
347 'email': username + '@example.com',
348 },
349 'grantedOn': int(granted_on)
350 }
Clark Boylanb640e052014-04-03 16:41:46 -0700351 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200352 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700353 del self.patchsets[-1]['approvals'][i]
354 self.patchsets[-1]['approvals'].append(approval)
355 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000356 'author': {'email': 'author@example.com',
357 'name': 'Patchset Author',
358 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700359 'change': {'branch': self.branch,
360 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
361 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000362 'owner': {'email': 'owner@example.com',
363 'name': 'Change Owner',
364 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700365 'project': self.project,
366 'subject': self.subject,
367 'topic': 'master',
368 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000369 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700370 'patchSet': self.patchsets[-1],
371 'type': 'comment-added'}
372 self.data['submitRecords'] = self.getSubmitRecords()
373 return json.loads(json.dumps(event))
374
375 def getSubmitRecords(self):
376 status = {}
377 for cat in self.categories.keys():
378 status[cat] = 0
379
380 for a in self.patchsets[-1]['approvals']:
381 cur = status[a['type']]
382 cat_min, cat_max = self.categories[a['type']][1:]
383 new = int(a['value'])
384 if new == cat_min:
385 cur = new
386 elif abs(new) > abs(cur):
387 cur = new
388 status[a['type']] = cur
389
390 labels = []
391 ok = True
392 for typ, cat in self.categories.items():
393 cur = status[typ]
394 cat_min, cat_max = cat[1:]
395 if cur == cat_min:
396 value = 'REJECT'
397 ok = False
398 elif cur == cat_max:
399 value = 'OK'
400 else:
401 value = 'NEED'
402 ok = False
403 labels.append({'label': cat[0], 'status': value})
404 if ok:
405 return [{'status': 'OK'}]
406 return [{'status': 'NOT_READY',
407 'labels': labels}]
408
409 def setDependsOn(self, other, patchset):
410 self.depends_on_change = other
411 d = {'id': other.data['id'],
412 'number': other.data['number'],
413 'ref': other.patchsets[patchset - 1]['ref']
414 }
415 self.data['dependsOn'] = [d]
416
417 other.needed_by_changes.append(self)
418 needed = other.data.get('neededBy', [])
419 d = {'id': self.data['id'],
420 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700421 'ref': self.patchsets[-1]['ref'],
422 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700423 }
424 needed.append(d)
425 other.data['neededBy'] = needed
426
427 def query(self):
428 self.queried += 1
429 d = self.data.get('dependsOn')
430 if d:
431 d = d[0]
432 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
433 d['isCurrentPatchSet'] = True
434 else:
435 d['isCurrentPatchSet'] = False
436 return json.loads(json.dumps(self.data))
437
438 def setMerged(self):
439 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000440 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700441 return
442 if self.fail_merge:
443 return
444 self.data['status'] = 'MERGED'
445 self.open = False
446
447 path = os.path.join(self.upstream_root, self.project)
448 repo = git.Repo(path)
449 repo.heads[self.branch].commit = \
450 repo.commit(self.patchsets[-1]['revision'])
451
452 def setReported(self):
453 self.reported += 1
454
455
James E. Blaire511d2f2016-12-08 15:22:26 -0800456class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700457 """A Fake Gerrit connection for use in tests.
458
459 This subclasses
460 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
461 ability for tests to add changes to the fake Gerrit it represents.
462 """
463
Joshua Hesketh352264b2015-08-11 23:42:08 +1000464 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700465
James E. Blaire511d2f2016-12-08 15:22:26 -0800466 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700467 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800468 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 connection_config)
470
Monty Taylorb934c1a2017-06-16 19:31:47 -0500471 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700472 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
473 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700475 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200476 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700477
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700478 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700479 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700480 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700481 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700482 c = FakeGerritChange(self, self.change_number, project, branch,
483 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700484 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700485 self.changes[self.change_number] = c
486 return c
487
James E. Blair72facdc2017-08-17 10:29:12 -0700488 def getFakeBranchCreatedEvent(self, project, branch):
489 path = os.path.join(self.upstream_root, project)
490 repo = git.Repo(path)
491 oldrev = 40 * '0'
492
493 event = {
494 "type": "ref-updated",
495 "submitter": {
496 "name": "User Name",
497 },
498 "refUpdate": {
499 "oldRev": oldrev,
500 "newRev": repo.heads[branch].commit.hexsha,
501 "refName": branch,
502 "project": project,
503 }
504 }
505 return event
506
Clark Boylanb640e052014-04-03 16:41:46 -0700507 def review(self, project, changeid, message, action):
508 number, ps = changeid.split(',')
509 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000510
511 # Add the approval back onto the change (ie simulate what gerrit would
512 # do).
513 # Usually when zuul leaves a review it'll create a feedback loop where
514 # zuul's review enters another gerrit event (which is then picked up by
515 # zuul). However, we can't mimic this behaviour (by adding this
516 # approval event into the queue) as it stops jobs from checking what
517 # happens before this event is triggered. If a job needs to see what
518 # happens they can add their own verified event into the queue.
519 # Nevertheless, we can update change with the new review in gerrit.
520
James E. Blair8b5408c2016-08-08 15:37:46 -0700521 for cat in action.keys():
522 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000523 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000524
Clark Boylanb640e052014-04-03 16:41:46 -0700525 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000526
Clark Boylanb640e052014-04-03 16:41:46 -0700527 if 'submit' in action:
528 change.setMerged()
529 if message:
530 change.setReported()
531
532 def query(self, number):
533 change = self.changes.get(int(number))
534 if change:
535 return change.query()
536 return {}
537
James E. Blairc494d542014-08-06 09:23:52 -0700538 def simpleQuery(self, query):
James E. Blair96698e22015-04-02 07:48:21 -0700539 self.log.debug("simpleQuery: %s" % query)
James E. Blairf8ff9932014-08-15 15:24:24 -0700540 self.queries.append(query)
James E. Blair5ee24252014-12-30 10:12:29 -0800541 if query.startswith('change:'):
542 # Query a specific changeid
543 changeid = query[len('change:'):]
544 l = [change.query() for change in self.changes.values()
545 if change.data['id'] == changeid]
James E. Blair96698e22015-04-02 07:48:21 -0700546 elif query.startswith('message:'):
547 # Query the content of a commit message
548 msg = query[len('message:'):].strip()
549 l = [change.query() for change in self.changes.values()
550 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800551 else:
552 # Query all open changes
553 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700554 return l
James E. Blairc494d542014-08-06 09:23:52 -0700555
Joshua Hesketh352264b2015-08-11 23:42:08 +1000556 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700557 pass
558
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200559 def _uploadPack(self, project):
560 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
561 'multi_ack thin-pack side-band side-band-64k ofs-delta '
562 'shallow no-progress include-tag multi_ack_detailed no-done\n')
563 path = os.path.join(self.upstream_root, project.name)
564 repo = git.Repo(path)
565 for ref in repo.refs:
566 r = ref.object.hexsha + ' ' + ref.path + '\n'
567 ret += '%04x%s' % (len(r) + 4, r)
568 ret += '0000'
569 return ret
570
Joshua Hesketh352264b2015-08-11 23:42:08 +1000571 def getGitUrl(self, project):
572 return os.path.join(self.upstream_root, project.name)
573
Clark Boylanb640e052014-04-03 16:41:46 -0700574
Gregory Haynes4fc12542015-04-22 20:38:06 -0700575class GithubChangeReference(git.Reference):
576 _common_path_default = "refs/pull"
577 _points_to_commits_only = True
578
579
Tobias Henkel64e37a02017-08-02 10:13:30 +0200580class FakeGithub(object):
581
582 class FakeUser(object):
583 def __init__(self, login):
584 self.login = login
585 self.name = "Github User"
586 self.email = "github.user@example.com"
587
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200588 class FakeBranch(object):
589 def __init__(self, branch='master'):
590 self.name = branch
591
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200592 class FakeStatus(object):
593 def __init__(self, state, url, description, context, user):
594 self._state = state
595 self._url = url
596 self._description = description
597 self._context = context
598 self._user = user
599
600 def as_dict(self):
601 return {
602 'state': self._state,
603 'url': self._url,
604 'description': self._description,
605 'context': self._context,
606 'creator': {
607 'login': self._user
608 }
609 }
610
611 class FakeCommit(object):
612 def __init__(self):
613 self._statuses = []
614
615 def set_status(self, state, url, description, context, user):
616 status = FakeGithub.FakeStatus(
617 state, url, description, context, user)
618 # always insert a status to the front of the list, to represent
619 # the last status provided for a commit.
620 self._statuses.insert(0, status)
621
622 def statuses(self):
623 return self._statuses
624
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200625 class FakeRepository(object):
626 def __init__(self):
627 self._branches = [FakeGithub.FakeBranch()]
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200628 self._commits = {}
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200629
Tobias Henkeleca46202017-08-02 20:27:10 +0200630 def branches(self, protected=False):
631 if protected:
632 # simulate there is no protected branch
633 return []
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200634 return self._branches
635
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200636 def create_status(self, sha, state, url, description, context,
637 user='zuul'):
638 # Since we're bypassing github API, which would require a user, we
639 # default the user as 'zuul' here.
640 commit = self._commits.get(sha, None)
641 if commit is None:
642 commit = FakeGithub.FakeCommit()
643 self._commits[sha] = commit
644 commit.set_status(state, url, description, context, user)
645
646 def commit(self, sha):
647 commit = self._commits.get(sha, None)
648 if commit is None:
649 commit = FakeGithub.FakeCommit()
650 self._commits[sha] = commit
651 return commit
652
653 def __init__(self):
654 self._repos = {}
655
Tobias Henkel64e37a02017-08-02 10:13:30 +0200656 def user(self, login):
657 return self.FakeUser(login)
658
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200659 def repository(self, owner, proj):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +0200660 return self._repos.get((owner, proj), None)
661
662 def repo_from_project(self, project):
663 # This is a convenience method for the tests.
664 owner, proj = project.split('/')
665 return self.repository(owner, proj)
666
667 def addProject(self, project):
668 owner, proj = project.name.split('/')
669 self._repos[(owner, proj)] = self.FakeRepository()
Tobias Henkel90b32ea2017-08-02 16:22:08 +0200670
Tobias Henkel64e37a02017-08-02 10:13:30 +0200671
Gregory Haynes4fc12542015-04-22 20:38:06 -0700672class FakeGithubPullRequest(object):
673
674 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800675 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700676 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700677 """Creates a new PR with several commits.
678 Sends an event about opened PR."""
679 self.github = github
680 self.source = github
681 self.number = number
682 self.project = project
683 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100684 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700685 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100686 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700687 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100688 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700689 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100690 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100691 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800692 self.reviews = []
693 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700694 self.updated_at = None
695 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100696 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100697 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700698 self.state = 'open'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700699 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100700 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700701 self._updateTimeStamp()
702
Jan Hruban570d01c2016-03-10 21:51:32 +0100703 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700704 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100705 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700706 self._updateTimeStamp()
707
Jan Hruban570d01c2016-03-10 21:51:32 +0100708 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700709 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100710 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700711 self._updateTimeStamp()
712
713 def getPullRequestOpenedEvent(self):
714 return self._getPullRequestEvent('opened')
715
716 def getPullRequestSynchronizeEvent(self):
717 return self._getPullRequestEvent('synchronize')
718
719 def getPullRequestReopenedEvent(self):
720 return self._getPullRequestEvent('reopened')
721
722 def getPullRequestClosedEvent(self):
723 return self._getPullRequestEvent('closed')
724
Jesse Keatinga41566f2017-06-14 18:17:51 -0700725 def getPullRequestEditedEvent(self):
726 return self._getPullRequestEvent('edited')
727
Gregory Haynes4fc12542015-04-22 20:38:06 -0700728 def addComment(self, message):
729 self.comments.append(message)
730 self._updateTimeStamp()
731
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200732 def getCommentAddedEvent(self, text):
733 name = 'issue_comment'
734 data = {
735 'action': 'created',
736 'issue': {
737 'number': self.number
738 },
739 'comment': {
740 'body': text
741 },
742 'repository': {
743 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100744 },
745 'sender': {
746 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200747 }
748 }
749 return (name, data)
750
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800751 def getReviewAddedEvent(self, review):
752 name = 'pull_request_review'
753 data = {
754 'action': 'submitted',
755 'pull_request': {
756 'number': self.number,
757 'title': self.subject,
758 'updated_at': self.updated_at,
759 'base': {
760 'ref': self.branch,
761 'repo': {
762 'full_name': self.project
763 }
764 },
765 'head': {
766 'sha': self.head_sha
767 }
768 },
769 'review': {
770 'state': review
771 },
772 'repository': {
773 'full_name': self.project
774 },
775 'sender': {
776 'login': 'ghuser'
777 }
778 }
779 return (name, data)
780
Jan Hruban16ad31f2015-11-07 14:39:07 +0100781 def addLabel(self, name):
782 if name not in self.labels:
783 self.labels.append(name)
784 self._updateTimeStamp()
785 return self._getLabelEvent(name)
786
787 def removeLabel(self, name):
788 if name in self.labels:
789 self.labels.remove(name)
790 self._updateTimeStamp()
791 return self._getUnlabelEvent(name)
792
793 def _getLabelEvent(self, label):
794 name = 'pull_request'
795 data = {
796 'action': 'labeled',
797 'pull_request': {
798 'number': self.number,
799 'updated_at': self.updated_at,
800 'base': {
801 'ref': self.branch,
802 'repo': {
803 'full_name': self.project
804 }
805 },
806 'head': {
807 'sha': self.head_sha
808 }
809 },
810 'label': {
811 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100812 },
813 'sender': {
814 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100815 }
816 }
817 return (name, data)
818
819 def _getUnlabelEvent(self, label):
820 name = 'pull_request'
821 data = {
822 'action': 'unlabeled',
823 'pull_request': {
824 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100825 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100826 'updated_at': self.updated_at,
827 'base': {
828 'ref': self.branch,
829 'repo': {
830 'full_name': self.project
831 }
832 },
833 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800834 'sha': self.head_sha,
835 'repo': {
836 'full_name': self.project
837 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100838 }
839 },
840 'label': {
841 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100842 },
843 'sender': {
844 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100845 }
846 }
847 return (name, data)
848
Jesse Keatinga41566f2017-06-14 18:17:51 -0700849 def editBody(self, body):
850 self.body = body
851 self._updateTimeStamp()
852
Gregory Haynes4fc12542015-04-22 20:38:06 -0700853 def _getRepo(self):
854 repo_path = os.path.join(self.upstream_root, self.project)
855 return git.Repo(repo_path)
856
857 def _createPRRef(self):
858 repo = self._getRepo()
859 GithubChangeReference.create(
860 repo, self._getPRReference(), 'refs/tags/init')
861
Jan Hruban570d01c2016-03-10 21:51:32 +0100862 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700863 repo = self._getRepo()
864 ref = repo.references[self._getPRReference()]
865 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100866 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700867 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100868 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700869 repo.head.reference = ref
870 zuul.merger.merger.reset_repo_to_head(repo)
871 repo.git.clean('-x', '-f', '-d')
872
Jan Hruban570d01c2016-03-10 21:51:32 +0100873 if files:
874 fn = files[0]
875 self.files = files
876 else:
877 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
878 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100879 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700880 fn = os.path.join(repo.working_dir, fn)
881 f = open(fn, 'w')
882 with open(fn, 'w') as f:
883 f.write("test %s %s\n" %
884 (self.branch, self.number))
885 repo.index.add([fn])
886
887 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800888 # Create an empty set of statuses for the given sha,
889 # each sha on a PR may have a status set on it
890 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700891 repo.head.reference = 'master'
892 zuul.merger.merger.reset_repo_to_head(repo)
893 repo.git.clean('-x', '-f', '-d')
894 repo.heads['master'].checkout()
895
896 def _updateTimeStamp(self):
897 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
898
899 def getPRHeadSha(self):
900 repo = self._getRepo()
901 return repo.references[self._getPRReference()].commit.hexsha
902
Jesse Keatingae4cd272017-01-30 17:10:44 -0800903 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800904 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
905 # convert the timestamp to a str format that would be returned
906 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800907
Adam Gandelmand81dd762017-02-09 15:15:49 -0800908 if granted_on:
909 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
910 submitted_at = time.strftime(
911 gh_time_format, granted_on.timetuple())
912 else:
913 # github timestamps only down to the second, so we need to make
914 # sure reviews that tests add appear to be added over a period of
915 # time in the past and not all at once.
916 if not self.reviews:
917 # the first review happens 10 mins ago
918 offset = 600
919 else:
920 # subsequent reviews happen 1 minute closer to now
921 offset = 600 - (len(self.reviews) * 60)
922
923 granted_on = datetime.datetime.utcfromtimestamp(
924 time.time() - offset)
925 submitted_at = time.strftime(
926 gh_time_format, granted_on.timetuple())
927
Jesse Keatingae4cd272017-01-30 17:10:44 -0800928 self.reviews.append({
929 'state': state,
930 'user': {
931 'login': user,
932 'email': user + "@derp.com",
933 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800934 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800935 })
936
Gregory Haynes4fc12542015-04-22 20:38:06 -0700937 def _getPRReference(self):
938 return '%s/head' % self.number
939
940 def _getPullRequestEvent(self, action):
941 name = 'pull_request'
942 data = {
943 'action': action,
944 'number': self.number,
945 'pull_request': {
946 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100947 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700948 'updated_at': self.updated_at,
949 'base': {
950 'ref': self.branch,
951 'repo': {
952 'full_name': self.project
953 }
954 },
955 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800956 'sha': self.head_sha,
957 'repo': {
958 'full_name': self.project
959 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700960 },
961 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100962 },
963 'sender': {
964 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700965 }
966 }
967 return (name, data)
968
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800969 def getCommitStatusEvent(self, context, state='success', user='zuul'):
970 name = 'status'
971 data = {
972 'state': state,
973 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700974 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800975 'description': 'Test results for %s: %s' % (self.head_sha, state),
976 'target_url': 'http://zuul/%s' % self.head_sha,
977 'branches': [],
978 'context': context,
979 'sender': {
980 'login': user
981 }
982 }
983 return (name, data)
984
James E. Blair289f5932017-07-27 15:02:29 -0700985 def setMerged(self, commit_message):
986 self.is_merged = True
987 self.merge_message = commit_message
988
989 repo = self._getRepo()
990 repo.heads[self.branch].commit = repo.commit(self.head_sha)
991
Gregory Haynes4fc12542015-04-22 20:38:06 -0700992
993class FakeGithubConnection(githubconnection.GithubConnection):
994 log = logging.getLogger("zuul.test.FakeGithubConnection")
995
996 def __init__(self, driver, connection_name, connection_config,
997 upstream_root=None):
998 super(FakeGithubConnection, self).__init__(driver, connection_name,
999 connection_config)
1000 self.connection_name = connection_name
1001 self.pr_number = 0
1002 self.pull_requests = []
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001003 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001004 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +01001005 self.merge_failure = False
1006 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +01001007 self.reports = []
Tobias Henkel64e37a02017-08-02 10:13:30 +02001008 self.github_client = FakeGithub()
1009
1010 def getGithubClient(self,
1011 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -06001012 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +02001013 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -07001014
Jesse Keatinga41566f2017-06-14 18:17:51 -07001015 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -07001016 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001017 self.pr_number += 1
1018 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +01001019 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001020 files=files, body=body)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001021 self.pull_requests.append(pull_request)
1022 return pull_request
1023
Jesse Keating71a47ff2017-06-06 11:36:43 -07001024 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
1025 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -07001026 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001027 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001028 if not new_rev:
1029 new_rev = random_sha1()
1030 name = 'push'
1031 data = {
1032 'ref': ref,
1033 'before': old_rev,
1034 'after': new_rev,
1035 'repository': {
1036 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001037 },
1038 'commits': [
1039 {
1040 'added': added_files,
1041 'removed': removed_files,
1042 'modified': modified_files
1043 }
1044 ]
Wayne1a78c612015-06-11 17:14:13 -07001045 }
1046 return (name, data)
1047
Gregory Haynes4fc12542015-04-22 20:38:06 -07001048 def emitEvent(self, event):
1049 """Emulates sending the GitHub webhook event to the connection."""
1050 port = self.webapp.server.socket.getsockname()[1]
1051 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001052 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001053 secret = self.connection_config['webhook_token']
1054 signature = githubconnection._sign_request(payload, secret)
1055 headers = {'X-Github-Event': name, 'X-Hub-Signature': signature}
Gregory Haynes4fc12542015-04-22 20:38:06 -07001056 req = urllib.request.Request(
1057 'http://localhost:%s/connection/%s/payload'
1058 % (port, self.connection_name),
1059 data=payload, headers=headers)
Tristan Cacqueray2bafb1f2017-06-12 07:10:26 +00001060 return urllib.request.urlopen(req)
Gregory Haynes4fc12542015-04-22 20:38:06 -07001061
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001062 def addProject(self, project):
1063 # use the original method here and additionally register it in the
1064 # fake github
1065 super(FakeGithubConnection, self).addProject(project)
1066 self.getGithubClient(project).addProject(project)
1067
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001068 def getPull(self, project, number):
1069 pr = self.pull_requests[number - 1]
1070 data = {
1071 'number': number,
Jan Hruban3b415922016-02-03 13:10:22 +01001072 'title': pr.subject,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001073 'updated_at': pr.updated_at,
1074 'base': {
1075 'repo': {
1076 'full_name': pr.project
1077 },
1078 'ref': pr.branch,
1079 },
Jan Hruban37615e52015-11-19 14:30:49 +01001080 'mergeable': True,
Jesse Keating4a27f132017-05-25 16:44:01 -07001081 'state': pr.state,
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001082 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -08001083 'sha': pr.head_sha,
1084 'repo': {
1085 'full_name': pr.project
1086 }
Jesse Keating61040e72017-06-08 15:08:27 -07001087 },
Jesse Keating19dfb492017-06-13 12:32:33 -07001088 'files': pr.files,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001089 'labels': pr.labels,
1090 'merged': pr.is_merged,
1091 'body': pr.body
Jan Hrubanc7ab1602015-10-14 15:29:33 +02001092 }
1093 return data
1094
Jesse Keating9021a012017-08-29 14:45:27 -07001095 def getPullBySha(self, sha, project):
1096 prs = list(set([p for p in self.pull_requests if
1097 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001098 if len(prs) > 1:
1099 raise Exception('Multiple pulls found with head sha: %s' % sha)
1100 pr = prs[0]
1101 return self.getPull(pr.project, pr.number)
1102
Jesse Keatingae4cd272017-01-30 17:10:44 -08001103 def _getPullReviews(self, owner, project, number):
1104 pr = self.pull_requests[number - 1]
1105 return pr.reviews
1106
Jesse Keatingae4cd272017-01-30 17:10:44 -08001107 def getRepoPermission(self, project, login):
1108 owner, proj = project.split('/')
1109 for pr in self.pull_requests:
1110 pr_owner, pr_project = pr.project.split('/')
1111 if (pr_owner == owner and proj == pr_project):
1112 if login in pr.writers:
1113 return 'write'
1114 else:
1115 return 'read'
1116
Gregory Haynes4fc12542015-04-22 20:38:06 -07001117 def getGitUrl(self, project):
1118 return os.path.join(self.upstream_root, str(project))
1119
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001120 def real_getGitUrl(self, project):
1121 return super(FakeGithubConnection, self).getGitUrl(project)
1122
Jan Hrubane252a732017-01-03 15:03:09 +01001123 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001124 # record that this got reported
1125 self.reports.append((project, pr_number, 'comment'))
Wayne40f40042015-06-12 16:56:30 -07001126 pull_request = self.pull_requests[pr_number - 1]
1127 pull_request.addComment(message)
1128
Jan Hruban3b415922016-02-03 13:10:22 +01001129 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001130 # record that this got reported
1131 self.reports.append((project, pr_number, 'merge'))
Jan Hruban49bff072015-11-03 11:45:46 +01001132 pull_request = self.pull_requests[pr_number - 1]
1133 if self.merge_failure:
1134 raise Exception('Pull request was not merged')
1135 if self.merge_not_allowed_count > 0:
1136 self.merge_not_allowed_count -= 1
1137 raise MergeFailure('Merge was not successful due to mergeability'
1138 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001139 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001140
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001141 def setCommitStatus(self, project, sha, state, url='', description='',
1142 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001143 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001144 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001145 super(FakeGithubConnection, self).setCommitStatus(
1146 project, sha, state,
1147 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001148
Jan Hruban16ad31f2015-11-07 14:39:07 +01001149 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001150 # record that this got reported
1151 self.reports.append((project, pr_number, 'label', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001152 pull_request = self.pull_requests[pr_number - 1]
1153 pull_request.addLabel(label)
1154
1155 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001156 # record that this got reported
1157 self.reports.append((project, pr_number, 'unlabel', label))
Jan Hruban16ad31f2015-11-07 14:39:07 +01001158 pull_request = self.pull_requests[pr_number - 1]
1159 pull_request.removeLabel(label)
1160
Jesse Keatinga41566f2017-06-14 18:17:51 -07001161 def _getNeededByFromPR(self, change):
1162 prs = []
1163 pattern = re.compile(r"Depends-On.*https://%s/%s/pull/%s" %
James E. Blair5f11ff32017-06-23 21:46:45 +01001164 (self.server, change.project.name,
Jesse Keatinga41566f2017-06-14 18:17:51 -07001165 change.number))
1166 for pr in self.pull_requests:
Jesse Keating152a4022017-07-07 08:39:52 -07001167 if not pr.body:
1168 body = ''
1169 else:
1170 body = pr.body
1171 if pattern.search(body):
Jesse Keatinga41566f2017-06-14 18:17:51 -07001172 # Get our version of a pull so that it's a dict
1173 pull = self.getPull(pr.project, pr.number)
1174 prs.append(pull)
1175
1176 return prs
1177
Gregory Haynes4fc12542015-04-22 20:38:06 -07001178
Clark Boylanb640e052014-04-03 16:41:46 -07001179class BuildHistory(object):
1180 def __init__(self, **kw):
1181 self.__dict__.update(kw)
1182
1183 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001184 return ("<Completed build, result: %s name: %s uuid: %s "
1185 "changes: %s ref: %s>" %
1186 (self.result, self.name, self.uuid,
1187 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001188
1189
Clark Boylanb640e052014-04-03 16:41:46 -07001190class FakeStatsd(threading.Thread):
1191 def __init__(self):
1192 threading.Thread.__init__(self)
1193 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001194 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001195 self.sock.bind(('', 0))
1196 self.port = self.sock.getsockname()[1]
1197 self.wake_read, self.wake_write = os.pipe()
1198 self.stats = []
1199
1200 def run(self):
1201 while True:
1202 poll = select.poll()
1203 poll.register(self.sock, select.POLLIN)
1204 poll.register(self.wake_read, select.POLLIN)
1205 ret = poll.poll()
1206 for (fd, event) in ret:
1207 if fd == self.sock.fileno():
1208 data = self.sock.recvfrom(1024)
1209 if not data:
1210 return
1211 self.stats.append(data[0])
1212 if fd == self.wake_read:
1213 return
1214
1215 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001216 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001217
1218
James E. Blaire1767bc2016-08-02 10:00:27 -07001219class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001220 log = logging.getLogger("zuul.test")
1221
Paul Belanger174a8272017-03-14 13:20:10 -04001222 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001223 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001224 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001225 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001226 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001227 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001228 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001229 # TODOv3(jeblair): self.node is really "the label of the node
1230 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001231 # keep using it like this, or we may end up exposing more of
1232 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001233 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001234 self.node = None
1235 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001236 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001237 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001238 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001239 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001240 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001241 self.wait_condition = threading.Condition()
1242 self.waiting = False
1243 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001244 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001245 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001246 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001247 items = self.parameters['zuul']['items']
1248 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1249 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001250
James E. Blair3158e282016-08-19 09:34:11 -07001251 def __repr__(self):
1252 waiting = ''
1253 if self.waiting:
1254 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001255 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1256 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001257
Clark Boylanb640e052014-04-03 16:41:46 -07001258 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001259 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001260 self.wait_condition.acquire()
1261 self.wait_condition.notify()
1262 self.waiting = False
1263 self.log.debug("Build %s released" % self.unique)
1264 self.wait_condition.release()
1265
1266 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001267 """Return whether this build is being held.
1268
1269 :returns: Whether the build is being held.
1270 :rtype: bool
1271 """
1272
Clark Boylanb640e052014-04-03 16:41:46 -07001273 self.wait_condition.acquire()
1274 if self.waiting:
1275 ret = True
1276 else:
1277 ret = False
1278 self.wait_condition.release()
1279 return ret
1280
1281 def _wait(self):
1282 self.wait_condition.acquire()
1283 self.waiting = True
1284 self.log.debug("Build %s waiting" % self.unique)
1285 self.wait_condition.wait()
1286 self.wait_condition.release()
1287
1288 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001289 self.log.debug('Running build %s' % self.unique)
1290
Paul Belanger174a8272017-03-14 13:20:10 -04001291 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001292 self.log.debug('Holding build %s' % self.unique)
1293 self._wait()
1294 self.log.debug("Build %s continuing" % self.unique)
1295
James E. Blair412fba82017-01-26 15:00:50 -08001296 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001297 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001298 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001299 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001300 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001301 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001302 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001303
James E. Blaire1767bc2016-08-02 10:00:27 -07001304 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001305
James E. Blaira5dba232016-08-08 15:53:24 -07001306 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001307 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001308 for change in changes:
1309 if self.hasChanges(change):
1310 return True
1311 return False
1312
James E. Blaire7b99a02016-08-05 14:27:34 -07001313 def hasChanges(self, *changes):
1314 """Return whether this build has certain changes in its git repos.
1315
1316 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001317 are expected to be present (in order) in the git repository of
1318 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001319
1320 :returns: Whether the build has the indicated changes.
1321 :rtype: bool
1322
1323 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001324 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001325 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001326 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001327 try:
1328 repo = git.Repo(path)
1329 except NoSuchPathError as e:
1330 self.log.debug('%s' % e)
1331 return False
James E. Blair247cab72017-07-20 16:52:36 -07001332 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001333 commit_message = '%s-1' % change.subject
1334 self.log.debug("Checking if build %s has changes; commit_message "
1335 "%s; repo_messages %s" % (self, commit_message,
1336 repo_messages))
1337 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001338 self.log.debug(" messages do not match")
1339 return False
1340 self.log.debug(" OK")
1341 return True
1342
James E. Blaird8af5422017-05-24 13:59:40 -07001343 def getWorkspaceRepos(self, projects):
1344 """Return workspace git repo objects for the listed projects
1345
1346 :arg list projects: A list of strings, each the canonical name
1347 of a project.
1348
1349 :returns: A dictionary of {name: repo} for every listed
1350 project.
1351 :rtype: dict
1352
1353 """
1354
1355 repos = {}
1356 for project in projects:
1357 path = os.path.join(self.jobdir.src_root, project)
1358 repo = git.Repo(path)
1359 repos[project] = repo
1360 return repos
1361
Clark Boylanb640e052014-04-03 16:41:46 -07001362
James E. Blair107bb252017-10-13 15:53:16 -07001363class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1364 def doMergeChanges(self, merger, items, repo_state):
1365 # Get a merger in order to update the repos involved in this job.
1366 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1367 merger, items, repo_state)
1368 if not commit: # merge conflict
1369 self.recordResult('MERGER_FAILURE')
1370 return commit
1371
1372 def recordResult(self, result):
1373 build = self.executor_server.job_builds[self.job.unique]
1374 self.executor_server.lock.acquire()
1375 self.executor_server.build_history.append(
1376 BuildHistory(name=build.name, result=result, changes=build.changes,
1377 node=build.node, uuid=build.unique,
1378 ref=build.parameters['zuul']['ref'],
1379 parameters=build.parameters, jobdir=build.jobdir,
1380 pipeline=build.parameters['zuul']['pipeline'])
1381 )
1382 self.executor_server.running_builds.remove(build)
1383 del self.executor_server.job_builds[self.job.unique]
1384 self.executor_server.lock.release()
1385
1386 def runPlaybooks(self, args):
1387 build = self.executor_server.job_builds[self.job.unique]
1388 build.jobdir = self.jobdir
1389
1390 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1391 self.recordResult(result)
1392 return result
1393
James E. Blaira86aaf12017-10-15 20:59:50 -07001394 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001395 build = self.executor_server.job_builds[self.job.unique]
1396
1397 if self.executor_server._run_ansible:
1398 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001399 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001400 else:
1401 if playbook.path:
1402 result = build.run()
1403 else:
1404 result = (self.RESULT_NORMAL, 0)
1405 return result
1406
1407 def getHostList(self, args):
1408 self.log.debug("hostlist")
1409 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1410 for host in hosts:
1411 host['host_vars']['ansible_connection'] = 'local'
1412
1413 hosts.append(dict(
1414 name='localhost',
1415 host_vars=dict(ansible_connection='local'),
1416 host_keys=[]))
1417 return hosts
1418
1419
Paul Belanger174a8272017-03-14 13:20:10 -04001420class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1421 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001422
Paul Belanger174a8272017-03-14 13:20:10 -04001423 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001424 they will report that they have started but then pause until
1425 released before reporting completion. This attribute may be
1426 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001427 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001428 be explicitly released.
1429
1430 """
James E. Blairfaf81982017-10-10 15:42:26 -07001431
1432 _job_class = RecordingAnsibleJob
1433
James E. Blairf5dbd002015-12-23 15:26:17 -08001434 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001435 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001436 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001437 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001438 self.hold_jobs_in_build = False
1439 self.lock = threading.Lock()
1440 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001441 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001442 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001443 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001444
James E. Blaira5dba232016-08-08 15:53:24 -07001445 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001446 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001447
1448 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001449 :arg Change change: The :py:class:`~tests.base.FakeChange`
1450 instance which should cause the job to fail. This job
1451 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001452
1453 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001454 l = self.fail_tests.get(name, [])
1455 l.append(change)
1456 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001457
James E. Blair962220f2016-08-03 11:22:38 -07001458 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001459 """Release a held build.
1460
1461 :arg str regex: A regular expression which, if supplied, will
1462 cause only builds with matching names to be released. If
1463 not supplied, all builds will be released.
1464
1465 """
James E. Blair962220f2016-08-03 11:22:38 -07001466 builds = self.running_builds[:]
1467 self.log.debug("Releasing build %s (%s)" % (regex,
1468 len(self.running_builds)))
1469 for build in builds:
1470 if not regex or re.match(regex, build.name):
1471 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001472 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001473 build.release()
1474 else:
1475 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001476 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001477 self.log.debug("Done releasing builds %s (%s)" %
1478 (regex, len(self.running_builds)))
1479
Paul Belanger174a8272017-03-14 13:20:10 -04001480 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001481 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001482 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001483 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001484 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001485 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001486 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001487 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001488 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001489
1490 def stopJob(self, job):
1491 self.log.debug("handle stop")
1492 parameters = json.loads(job.arguments)
1493 uuid = parameters['uuid']
1494 for build in self.running_builds:
1495 if build.unique == uuid:
1496 build.aborted = True
1497 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001498 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001499
James E. Blaira002b032017-04-18 10:35:48 -07001500 def stop(self):
1501 for build in self.running_builds:
1502 build.release()
1503 super(RecordingExecutorServer, self).stop()
1504
Joshua Hesketh50c21782016-10-13 21:34:14 +11001505
Clark Boylanb640e052014-04-03 16:41:46 -07001506class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001507 """A Gearman server for use in tests.
1508
1509 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1510 added to the queue but will not be distributed to workers
1511 until released. This attribute may be changed at any time and
1512 will take effect for subsequently enqueued jobs, but
1513 previously held jobs will still need to be explicitly
1514 released.
1515
1516 """
1517
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001518 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001519 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001520 self.hold_merge_jobs_in_queue = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001521 if use_ssl:
1522 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1523 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1524 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1525 else:
1526 ssl_ca = None
1527 ssl_cert = None
1528 ssl_key = None
1529
1530 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1531 ssl_cert=ssl_cert,
1532 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001533
1534 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001535 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1536 for job in job_queue:
Clark Boylanb640e052014-04-03 16:41:46 -07001537 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001538 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001539 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001540 elif job.name.startswith(b'merger:'):
1541 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001542 else:
1543 job.waiting = False
1544 if job.waiting:
1545 continue
1546 if job.name in connection.functions:
1547 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001548 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001549 connection.related_jobs[job.handle] = job
1550 job.worker_connection = connection
1551 job.running = True
1552 return job
1553 return None
1554
1555 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001556 """Release a held job.
1557
1558 :arg str regex: A regular expression which, if supplied, will
1559 cause only jobs with matching names to be released. If
1560 not supplied, all jobs will be released.
1561 """
Clark Boylanb640e052014-04-03 16:41:46 -07001562 released = False
1563 qlen = (len(self.high_queue) + len(self.normal_queue) +
1564 len(self.low_queue))
1565 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1566 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001567 match = False
1568 if job.name == b'executor:execute':
1569 parameters = json.loads(job.arguments.decode('utf8'))
1570 if not regex or re.match(regex, parameters.get('job')):
1571 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001572 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001573 if not regex:
1574 match = True
1575 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001576 self.log.debug("releasing queued job %s" %
1577 job.unique)
1578 job.waiting = False
1579 released = True
1580 else:
1581 self.log.debug("not releasing queued job %s" %
1582 job.unique)
1583 if released:
1584 self.wakeConnections()
1585 qlen = (len(self.high_queue) + len(self.normal_queue) +
1586 len(self.low_queue))
1587 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1588
1589
1590class FakeSMTP(object):
1591 log = logging.getLogger('zuul.FakeSMTP')
1592
1593 def __init__(self, messages, server, port):
1594 self.server = server
1595 self.port = port
1596 self.messages = messages
1597
1598 def sendmail(self, from_email, to_email, msg):
1599 self.log.info("Sending email from %s, to %s, with msg %s" % (
1600 from_email, to_email, msg))
1601
1602 headers = msg.split('\n\n', 1)[0]
1603 body = msg.split('\n\n', 1)[1]
1604
1605 self.messages.append(dict(
1606 from_email=from_email,
1607 to_email=to_email,
1608 msg=msg,
1609 headers=headers,
1610 body=body,
1611 ))
1612
1613 return True
1614
1615 def quit(self):
1616 return True
1617
1618
James E. Blairdce6cea2016-12-20 16:45:32 -08001619class FakeNodepool(object):
1620 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001621 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001622
1623 log = logging.getLogger("zuul.test.FakeNodepool")
1624
1625 def __init__(self, host, port, chroot):
1626 self.client = kazoo.client.KazooClient(
1627 hosts='%s:%s%s' % (host, port, chroot))
1628 self.client.start()
1629 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001630 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001631 self.thread = threading.Thread(target=self.run)
1632 self.thread.daemon = True
1633 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001634 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001635
1636 def stop(self):
1637 self._running = False
1638 self.thread.join()
1639 self.client.stop()
1640 self.client.close()
1641
1642 def run(self):
1643 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001644 try:
1645 self._run()
1646 except Exception:
1647 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001648 time.sleep(0.1)
1649
1650 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001651 if self.paused:
1652 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001653 for req in self.getNodeRequests():
1654 self.fulfillRequest(req)
1655
1656 def getNodeRequests(self):
1657 try:
1658 reqids = self.client.get_children(self.REQUEST_ROOT)
1659 except kazoo.exceptions.NoNodeError:
1660 return []
1661 reqs = []
1662 for oid in sorted(reqids):
1663 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001664 try:
1665 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001666 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001667 data['_oid'] = oid
1668 reqs.append(data)
1669 except kazoo.exceptions.NoNodeError:
1670 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001671 return reqs
1672
James E. Blaire18d4602017-01-05 11:17:28 -08001673 def getNodes(self):
1674 try:
1675 nodeids = self.client.get_children(self.NODE_ROOT)
1676 except kazoo.exceptions.NoNodeError:
1677 return []
1678 nodes = []
1679 for oid in sorted(nodeids):
1680 path = self.NODE_ROOT + '/' + oid
1681 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001682 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001683 data['_oid'] = oid
1684 try:
1685 lockfiles = self.client.get_children(path + '/lock')
1686 except kazoo.exceptions.NoNodeError:
1687 lockfiles = []
1688 if lockfiles:
1689 data['_lock'] = True
1690 else:
1691 data['_lock'] = False
1692 nodes.append(data)
1693 return nodes
1694
James E. Blaira38c28e2017-01-04 10:33:20 -08001695 def makeNode(self, request_id, node_type):
1696 now = time.time()
1697 path = '/nodepool/nodes/'
1698 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001699 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001700 provider='test-provider',
1701 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001702 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001703 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001704 public_ipv4='127.0.0.1',
1705 private_ipv4=None,
1706 public_ipv6=None,
1707 allocated_to=request_id,
1708 state='ready',
1709 state_time=now,
1710 created_time=now,
1711 updated_time=now,
1712 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001713 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001714 executor='fake-nodepool')
Clint Byrumf322fe22017-05-10 20:53:12 -07001715 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001716 path = self.client.create(path, data,
1717 makepath=True,
1718 sequence=True)
1719 nodeid = path.split("/")[-1]
1720 return nodeid
1721
James E. Blair6ab79e02017-01-06 10:10:17 -08001722 def addFailRequest(self, request):
1723 self.fail_requests.add(request['_oid'])
1724
James E. Blairdce6cea2016-12-20 16:45:32 -08001725 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001726 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001727 return
1728 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001729 oid = request['_oid']
1730 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001731
James E. Blair6ab79e02017-01-06 10:10:17 -08001732 if oid in self.fail_requests:
1733 request['state'] = 'failed'
1734 else:
1735 request['state'] = 'fulfilled'
1736 nodes = []
1737 for node in request['node_types']:
1738 nodeid = self.makeNode(oid, node)
1739 nodes.append(nodeid)
1740 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001741
James E. Blaira38c28e2017-01-04 10:33:20 -08001742 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001743 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001744 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001745 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001746 try:
1747 self.client.set(path, data)
1748 except kazoo.exceptions.NoNodeError:
1749 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001750
1751
James E. Blair498059b2016-12-20 13:50:13 -08001752class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001753 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001754 super(ChrootedKazooFixture, self).__init__()
1755
1756 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1757 if ':' in zk_host:
1758 host, port = zk_host.split(':')
1759 else:
1760 host = zk_host
1761 port = None
1762
1763 self.zookeeper_host = host
1764
1765 if not port:
1766 self.zookeeper_port = 2181
1767 else:
1768 self.zookeeper_port = int(port)
1769
Clark Boylan621ec9a2017-04-07 17:41:33 -07001770 self.test_id = test_id
1771
James E. Blair498059b2016-12-20 13:50:13 -08001772 def _setUp(self):
1773 # Make sure the test chroot paths do not conflict
1774 random_bits = ''.join(random.choice(string.ascii_lowercase +
1775 string.ascii_uppercase)
1776 for x in range(8))
1777
Clark Boylan621ec9a2017-04-07 17:41:33 -07001778 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001779 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1780
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001781 self.addCleanup(self._cleanup)
1782
James E. Blair498059b2016-12-20 13:50:13 -08001783 # Ensure the chroot path exists and clean up any pre-existing znodes.
1784 _tmp_client = kazoo.client.KazooClient(
1785 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1786 _tmp_client.start()
1787
1788 if _tmp_client.exists(self.zookeeper_chroot):
1789 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1790
1791 _tmp_client.ensure_path(self.zookeeper_chroot)
1792 _tmp_client.stop()
1793 _tmp_client.close()
1794
James E. Blair498059b2016-12-20 13:50:13 -08001795 def _cleanup(self):
1796 '''Remove the chroot path.'''
1797 # Need a non-chroot'ed client to remove the chroot path
1798 _tmp_client = kazoo.client.KazooClient(
1799 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1800 _tmp_client.start()
1801 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1802 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001803 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001804
1805
Joshua Heskethd78b4482015-09-14 16:56:34 -06001806class MySQLSchemaFixture(fixtures.Fixture):
1807 def setUp(self):
1808 super(MySQLSchemaFixture, self).setUp()
1809
1810 random_bits = ''.join(random.choice(string.ascii_lowercase +
1811 string.ascii_uppercase)
1812 for x in range(8))
1813 self.name = '%s_%s' % (random_bits, os.getpid())
1814 self.passwd = uuid.uuid4().hex
1815 db = pymysql.connect(host="localhost",
1816 user="openstack_citest",
1817 passwd="openstack_citest",
1818 db="openstack_citest")
1819 cur = db.cursor()
1820 cur.execute("create database %s" % self.name)
1821 cur.execute(
1822 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1823 (self.name, self.name, self.passwd))
1824 cur.execute("flush privileges")
1825
1826 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1827 self.passwd,
1828 self.name)
1829 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1830 self.addCleanup(self.cleanup)
1831
1832 def cleanup(self):
1833 db = pymysql.connect(host="localhost",
1834 user="openstack_citest",
1835 passwd="openstack_citest",
1836 db="openstack_citest")
1837 cur = db.cursor()
1838 cur.execute("drop database %s" % self.name)
1839 cur.execute("drop user '%s'@'localhost'" % self.name)
1840 cur.execute("flush privileges")
1841
1842
Maru Newby3fe5f852015-01-13 04:22:14 +00001843class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001844 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001845 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001846
James E. Blair1c236df2017-02-01 14:07:24 -08001847 def attachLogs(self, *args):
1848 def reader():
1849 self._log_stream.seek(0)
1850 while True:
1851 x = self._log_stream.read(4096)
1852 if not x:
1853 break
1854 yield x.encode('utf8')
1855 content = testtools.content.content_from_reader(
1856 reader,
1857 testtools.content_type.UTF8_TEXT,
1858 False)
1859 self.addDetail('logging', content)
1860
Clark Boylanb640e052014-04-03 16:41:46 -07001861 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001862 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001863 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1864 try:
1865 test_timeout = int(test_timeout)
1866 except ValueError:
1867 # If timeout value is invalid do not set a timeout.
1868 test_timeout = 0
1869 if test_timeout > 0:
1870 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1871
1872 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1873 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1874 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1875 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1876 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1877 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1878 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1879 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1880 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1881 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001882 self._log_stream = StringIO()
1883 self.addOnException(self.attachLogs)
1884 else:
1885 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001886
James E. Blair73b41772017-05-22 13:22:55 -07001887 # NOTE(jeblair): this is temporary extra debugging to try to
1888 # track down a possible leak.
1889 orig_git_repo_init = git.Repo.__init__
1890
1891 def git_repo_init(myself, *args, **kw):
1892 orig_git_repo_init(myself, *args, **kw)
1893 self.log.debug("Created git repo 0x%x %s" %
1894 (id(myself), repr(myself)))
1895
1896 self.useFixture(fixtures.MonkeyPatch('git.Repo.__init__',
1897 git_repo_init))
1898
James E. Blair1c236df2017-02-01 14:07:24 -08001899 handler = logging.StreamHandler(self._log_stream)
1900 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1901 '%(levelname)-8s %(message)s')
1902 handler.setFormatter(formatter)
1903
1904 logger = logging.getLogger()
1905 logger.setLevel(logging.DEBUG)
1906 logger.addHandler(handler)
1907
Clark Boylan3410d532017-04-25 12:35:29 -07001908 # Make sure we don't carry old handlers around in process state
1909 # which slows down test runs
1910 self.addCleanup(logger.removeHandler, handler)
1911 self.addCleanup(handler.close)
1912 self.addCleanup(handler.flush)
1913
James E. Blair1c236df2017-02-01 14:07:24 -08001914 # NOTE(notmorgan): Extract logging overrides for specific
1915 # libraries from the OS_LOG_DEFAULTS env and create loggers
1916 # for each. This is used to limit the output during test runs
1917 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001918 log_defaults_from_env = os.environ.get(
1919 'OS_LOG_DEFAULTS',
Monty Taylor73db6492017-05-18 17:31:17 -05001920 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO,paste=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001921
James E. Blairdce6cea2016-12-20 16:45:32 -08001922 if log_defaults_from_env:
1923 for default in log_defaults_from_env.split(','):
1924 try:
1925 name, level_str = default.split('=', 1)
1926 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001927 logger = logging.getLogger(name)
1928 logger.setLevel(level)
1929 logger.addHandler(handler)
1930 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001931 except ValueError:
1932 # NOTE(notmorgan): Invalid format of the log default,
1933 # skip and don't try and apply a logger for the
1934 # specified module
1935 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001936
Maru Newby3fe5f852015-01-13 04:22:14 +00001937
1938class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001939 """A test case with a functioning Zuul.
1940
1941 The following class variables are used during test setup and can
1942 be overidden by subclasses but are effectively read-only once a
1943 test method starts running:
1944
1945 :cvar str config_file: This points to the main zuul config file
1946 within the fixtures directory. Subclasses may override this
1947 to obtain a different behavior.
1948
1949 :cvar str tenant_config_file: This is the tenant config file
1950 (which specifies from what git repos the configuration should
1951 be loaded). It defaults to the value specified in
1952 `config_file` but can be overidden by subclasses to obtain a
1953 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001954 configuration. See also the :py:func:`simple_layout`
1955 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001956
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001957 :cvar bool create_project_keys: Indicates whether Zuul should
1958 auto-generate keys for each project, or whether the test
1959 infrastructure should insert dummy keys to save time during
1960 startup. Defaults to False.
1961
James E. Blaire7b99a02016-08-05 14:27:34 -07001962 The following are instance variables that are useful within test
1963 methods:
1964
1965 :ivar FakeGerritConnection fake_<connection>:
1966 A :py:class:`~tests.base.FakeGerritConnection` will be
1967 instantiated for each connection present in the config file
1968 and stored here. For instance, `fake_gerrit` will hold the
1969 FakeGerritConnection object for a connection named `gerrit`.
1970
1971 :ivar FakeGearmanServer gearman_server: An instance of
1972 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1973 server that all of the Zuul components in this test use to
1974 communicate with each other.
1975
Paul Belanger174a8272017-03-14 13:20:10 -04001976 :ivar RecordingExecutorServer executor_server: An instance of
1977 :py:class:`~tests.base.RecordingExecutorServer` which is the
1978 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001979
1980 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1981 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001982 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001983 list upon completion.
1984
1985 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1986 objects representing completed builds. They are appended to
1987 the list in the order they complete.
1988
1989 """
1990
James E. Blair83005782015-12-11 14:46:03 -08001991 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07001992 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001993 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001994 use_ssl = False
James E. Blair3f876d52016-07-22 13:07:14 -07001995
1996 def _startMerger(self):
1997 self.merge_server = zuul.merger.server.MergeServer(self.config,
1998 self.connections)
1999 self.merge_server.start()
2000
Maru Newby3fe5f852015-01-13 04:22:14 +00002001 def setUp(self):
2002 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002003
2004 self.setupZK()
2005
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002006 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002007 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002008 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2009 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002010 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002011 tmp_root = tempfile.mkdtemp(
2012 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002013 self.test_root = os.path.join(tmp_root, "zuul-test")
2014 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002015 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002016 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002017 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002018 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2019 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002020
2021 if os.path.exists(self.test_root):
2022 shutil.rmtree(self.test_root)
2023 os.makedirs(self.test_root)
2024 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002025 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002026 os.makedirs(self.merger_state_root)
2027 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002028
2029 # Make per test copy of Configuration.
2030 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002031 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2032 if not os.path.exists(self.private_key_file):
2033 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2034 shutil.copy(src_private_key_file, self.private_key_file)
2035 shutil.copy('{}.pub'.format(src_private_key_file),
2036 '{}.pub'.format(self.private_key_file))
2037 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002038 self.config.set('scheduler', 'tenant_config',
2039 os.path.join(
2040 FIXTURE_DIR,
2041 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002042 self.config.set('scheduler', 'state_dir', self.state_root)
Monty Taylord642d852017-02-23 14:05:42 -05002043 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002044 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002045 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002046 self.config.set('executor', 'state_dir', self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002047
Clark Boylanb640e052014-04-03 16:41:46 -07002048 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07002049 if self.config.has_section('statsd'):
2050 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002051 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002052
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002053 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002054
2055 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002056 self.log.info("Gearman server on port %s" %
2057 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002058 if self.use_ssl:
2059 self.log.info('SSL enabled for gearman')
2060 self.config.set(
2061 'gearman', 'ssl_ca',
2062 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2063 self.config.set(
2064 'gearman', 'ssl_cert',
2065 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2066 self.config.set(
2067 'gearman', 'ssl_key',
2068 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002069
James E. Blaire511d2f2016-12-08 15:22:26 -08002070 gerritsource.GerritSource.replication_timeout = 1.5
2071 gerritsource.GerritSource.replication_retry_interval = 0.5
2072 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002073
Joshua Hesketh352264b2015-08-11 23:42:08 +10002074 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002075 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002076
Jan Hruban7083edd2015-08-21 14:00:54 +02002077 self.webapp = zuul.webapp.WebApp(
2078 self.sched, port=0, listen_address='127.0.0.1')
2079
Jan Hruban6b71aff2015-10-22 16:58:08 +02002080 self.event_queues = [
2081 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002082 self.sched.trigger_event_queue,
2083 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002084 ]
2085
James E. Blairfef78942016-03-11 16:28:56 -08002086 self.configure_connections()
Jan Hruban7083edd2015-08-21 14:00:54 +02002087 self.sched.registerConnections(self.connections, self.webapp)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002088
Paul Belanger174a8272017-03-14 13:20:10 -04002089 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002090 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002091 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002092 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002093 _test_root=self.test_root,
2094 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002095 self.executor_server.start()
2096 self.history = self.executor_server.build_history
2097 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002098
Paul Belanger174a8272017-03-14 13:20:10 -04002099 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002100 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002101 self.merge_client = zuul.merger.client.MergeClient(
2102 self.config, self.sched)
James E. Blair8d692392016-04-08 17:47:58 -07002103 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002104 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002105 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002106
James E. Blair0d5a36e2017-02-21 10:53:44 -05002107 self.fake_nodepool = FakeNodepool(
2108 self.zk_chroot_fixture.zookeeper_host,
2109 self.zk_chroot_fixture.zookeeper_port,
2110 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002111
Paul Belanger174a8272017-03-14 13:20:10 -04002112 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002113 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002114 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002115 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002116
Clark Boylanb640e052014-04-03 16:41:46 -07002117 self.sched.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002118 self.webapp.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002119 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002120 # Cleanups are run in reverse order
2121 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002122 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002123 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002124
James E. Blairb9c0d772017-03-03 14:34:49 -08002125 self.sched.reconfigure(self.config)
2126 self.sched.resume()
2127
Tobias Henkel7df274b2017-05-26 17:41:11 +02002128 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002129 # Set up gerrit related fakes
2130 # Set a changes database so multiple FakeGerrit's can report back to
2131 # a virtual canonical database given by the configured hostname
2132 self.gerrit_changes_dbs = {}
2133
2134 def getGerritConnection(driver, name, config):
2135 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2136 con = FakeGerritConnection(driver, name, config,
2137 changes_db=db,
2138 upstream_root=self.upstream_root)
2139 self.event_queues.append(con.event_queue)
2140 setattr(self, 'fake_' + name, con)
2141 return con
2142
2143 self.useFixture(fixtures.MonkeyPatch(
2144 'zuul.driver.gerrit.GerritDriver.getConnection',
2145 getGerritConnection))
2146
Gregory Haynes4fc12542015-04-22 20:38:06 -07002147 def getGithubConnection(driver, name, config):
2148 con = FakeGithubConnection(driver, name, config,
2149 upstream_root=self.upstream_root)
Jesse Keating64d29012017-09-06 12:27:49 -07002150 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002151 setattr(self, 'fake_' + name, con)
2152 return con
2153
2154 self.useFixture(fixtures.MonkeyPatch(
2155 'zuul.driver.github.GithubDriver.getConnection',
2156 getGithubConnection))
2157
James E. Blaire511d2f2016-12-08 15:22:26 -08002158 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002159 # TODO(jhesketh): This should come from lib.connections for better
2160 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002161 # Register connections from the config
2162 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002163
Joshua Hesketh352264b2015-08-11 23:42:08 +10002164 def FakeSMTPFactory(*args, **kw):
2165 args = [self.smtp_messages] + list(args)
2166 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002167
Joshua Hesketh352264b2015-08-11 23:42:08 +10002168 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002169
James E. Blaire511d2f2016-12-08 15:22:26 -08002170 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002171 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002172 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002173
James E. Blair83005782015-12-11 14:46:03 -08002174 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002175 # This creates the per-test configuration object. It can be
2176 # overriden by subclasses, but should not need to be since it
2177 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002178 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002179 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002180
James E. Blair39840362017-06-23 20:34:02 +01002181 sections = ['zuul', 'scheduler', 'executor', 'merger']
2182 for section in sections:
2183 if not self.config.has_section(section):
2184 self.config.add_section(section)
2185
James E. Blair06cc3922017-04-19 10:08:10 -07002186 if not self.setupSimpleLayout():
2187 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002188 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002189 self.tenant_config_file)
2190 git_path = os.path.join(
2191 os.path.dirname(
2192 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2193 'git')
2194 if os.path.exists(git_path):
2195 for reponame in os.listdir(git_path):
2196 project = reponame.replace('_', '/')
2197 self.copyDirToRepo(project,
2198 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002199 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002200 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002201 self.setupAllProjectKeys()
2202
James E. Blair06cc3922017-04-19 10:08:10 -07002203 def setupSimpleLayout(self):
2204 # If the test method has been decorated with a simple_layout,
2205 # use that instead of the class tenant_config_file. Set up a
2206 # single config-project with the specified layout, and
2207 # initialize repos for all of the 'project' entries which
2208 # appear in the layout.
2209 test_name = self.id().split('.')[-1]
2210 test = getattr(self, test_name)
2211 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002212 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002213 else:
2214 return False
2215
James E. Blairb70e55a2017-04-19 12:57:02 -07002216 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002217 path = os.path.join(FIXTURE_DIR, path)
2218 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002219 data = f.read()
2220 layout = yaml.safe_load(data)
2221 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002222 untrusted_projects = []
2223 for item in layout:
2224 if 'project' in item:
2225 name = item['project']['name']
2226 untrusted_projects.append(name)
2227 self.init_repo(name)
2228 self.addCommitToRepo(name, 'initial commit',
2229 files={'README': ''},
2230 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002231 if 'job' in item:
2232 jobname = item['job']['name']
2233 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002234
2235 root = os.path.join(self.test_root, "config")
2236 if not os.path.exists(root):
2237 os.makedirs(root)
2238 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2239 config = [{'tenant':
2240 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002241 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002242 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002243 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002244 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002245 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002246 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002247 os.path.join(FIXTURE_DIR, f.name))
2248
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002249 self.init_repo('org/common-config')
2250 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002251 files, branch='master', tag='init')
2252
2253 return True
2254
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002255 def setupAllProjectKeys(self):
2256 if self.create_project_keys:
2257 return
2258
James E. Blair39840362017-06-23 20:34:02 +01002259 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002260 with open(os.path.join(FIXTURE_DIR, path)) as f:
2261 tenant_config = yaml.safe_load(f.read())
2262 for tenant in tenant_config:
2263 sources = tenant['tenant']['source']
2264 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002265 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002266 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002267 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002268 self.setupProjectKeys(source, project)
2269
2270 def setupProjectKeys(self, source, project):
2271 # Make sure we set up an RSA key for the project so that we
2272 # don't spend time generating one:
2273
James E. Blair6459db12017-06-29 14:57:20 -07002274 if isinstance(project, dict):
2275 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002276 key_root = os.path.join(self.state_root, 'keys')
2277 if not os.path.isdir(key_root):
2278 os.mkdir(key_root, 0o700)
2279 private_key_file = os.path.join(key_root, source, project + '.pem')
2280 private_key_dir = os.path.dirname(private_key_file)
2281 self.log.debug("Installing test keys for project %s at %s" % (
2282 project, private_key_file))
2283 if not os.path.isdir(private_key_dir):
2284 os.makedirs(private_key_dir)
2285 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2286 with open(private_key_file, 'w') as o:
2287 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002288
James E. Blair498059b2016-12-20 13:50:13 -08002289 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002290 self.zk_chroot_fixture = self.useFixture(
2291 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002292 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002293 self.zk_chroot_fixture.zookeeper_host,
2294 self.zk_chroot_fixture.zookeeper_port,
2295 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002296
James E. Blair96c6bf82016-01-15 16:20:40 -08002297 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002298 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002299
2300 files = {}
2301 for (dirpath, dirnames, filenames) in os.walk(source_path):
2302 for filename in filenames:
2303 test_tree_filepath = os.path.join(dirpath, filename)
2304 common_path = os.path.commonprefix([test_tree_filepath,
2305 source_path])
2306 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2307 with open(test_tree_filepath, 'r') as f:
2308 content = f.read()
2309 files[relative_filepath] = content
2310 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002311 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002312
James E. Blaire18d4602017-01-05 11:17:28 -08002313 def assertNodepoolState(self):
2314 # Make sure that there are no pending requests
2315
2316 requests = self.fake_nodepool.getNodeRequests()
2317 self.assertEqual(len(requests), 0)
2318
2319 nodes = self.fake_nodepool.getNodes()
2320 for node in nodes:
2321 self.assertFalse(node['_lock'], "Node %s is locked" %
2322 (node['_oid'],))
2323
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002324 def assertNoGeneratedKeys(self):
2325 # Make sure that Zuul did not generate any project keys
2326 # (unless it was supposed to).
2327
2328 if self.create_project_keys:
2329 return
2330
2331 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2332 test_key = i.read()
2333
2334 key_root = os.path.join(self.state_root, 'keys')
2335 for root, dirname, files in os.walk(key_root):
2336 for fn in files:
2337 with open(os.path.join(root, fn)) as f:
2338 self.assertEqual(test_key, f.read())
2339
Clark Boylanb640e052014-04-03 16:41:46 -07002340 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002341 self.log.debug("Assert final state")
2342 # Make sure no jobs are running
2343 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002344 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002345 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002346 gc.collect()
2347 for obj in gc.get_objects():
2348 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002349 self.log.debug("Leaked git repo object: 0x%x %s" %
2350 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002351 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002352 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002353 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002354 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002355 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002356 for tenant in self.sched.abide.tenants.values():
2357 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002358 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002359 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002360
2361 def shutdown(self):
2362 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002363 self.executor_server.hold_jobs_in_build = False
2364 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002365 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002366 self.merge_client.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002367 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002368 self.sched.stop()
2369 self.sched.join()
2370 self.statsd.stop()
2371 self.statsd.join()
2372 self.webapp.stop()
2373 self.webapp.join()
Clark Boylanb640e052014-04-03 16:41:46 -07002374 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002375 self.fake_nodepool.stop()
2376 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002377 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002378 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002379 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002380 # Further the pydevd threads also need to be whitelisted so debugging
2381 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002382 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002383 'pydevd.CommandThread',
2384 'pydevd.Reader',
2385 'pydevd.Writer',
2386 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002387 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002388 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002389 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002390 log_str = ""
2391 for thread_id, stack_frame in sys._current_frames().items():
2392 log_str += "Thread: %s\n" % thread_id
2393 log_str += "".join(traceback.format_stack(stack_frame))
2394 self.log.debug(log_str)
2395 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002396
James E. Blaira002b032017-04-18 10:35:48 -07002397 def assertCleanShutdown(self):
2398 pass
2399
James E. Blairc4ba97a2017-04-19 16:26:24 -07002400 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002401 parts = project.split('/')
2402 path = os.path.join(self.upstream_root, *parts[:-1])
2403 if not os.path.exists(path):
2404 os.makedirs(path)
2405 path = os.path.join(self.upstream_root, project)
2406 repo = git.Repo.init(path)
2407
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002408 with repo.config_writer() as config_writer:
2409 config_writer.set_value('user', 'email', 'user@example.com')
2410 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002411
Clark Boylanb640e052014-04-03 16:41:46 -07002412 repo.index.commit('initial commit')
2413 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002414 if tag:
2415 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002416
James E. Blair97d902e2014-08-21 13:25:56 -07002417 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002418 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002419 repo.git.clean('-x', '-f', '-d')
2420
James E. Blair97d902e2014-08-21 13:25:56 -07002421 def create_branch(self, project, branch):
2422 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002423 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002424 fn = os.path.join(path, 'README')
2425
2426 branch_head = repo.create_head(branch)
2427 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002428 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002429 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002430 f.close()
2431 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002432 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002433
James E. Blair97d902e2014-08-21 13:25:56 -07002434 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002435 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002436 repo.git.clean('-x', '-f', '-d')
2437
Sachi King9f16d522016-03-16 12:20:45 +11002438 def create_commit(self, project):
2439 path = os.path.join(self.upstream_root, project)
2440 repo = git.Repo(path)
2441 repo.head.reference = repo.heads['master']
2442 file_name = os.path.join(path, 'README')
2443 with open(file_name, 'a') as f:
2444 f.write('creating fake commit\n')
2445 repo.index.add([file_name])
2446 commit = repo.index.commit('Creating a fake commit')
2447 return commit.hexsha
2448
James E. Blairf4a5f022017-04-18 14:01:10 -07002449 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002450 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002451 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002452 while len(self.builds):
2453 self.release(self.builds[0])
2454 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002455 i += 1
2456 if count is not None and i >= count:
2457 break
James E. Blairb8c16472015-05-05 14:55:26 -07002458
James E. Blairdf25ddc2017-07-08 07:57:09 -07002459 def getSortedBuilds(self):
2460 "Return the list of currently running builds sorted by name"
2461
2462 return sorted(self.builds, key=lambda x: x.name)
2463
Clark Boylanb640e052014-04-03 16:41:46 -07002464 def release(self, job):
2465 if isinstance(job, FakeBuild):
2466 job.release()
2467 else:
2468 job.waiting = False
2469 self.log.debug("Queued job %s released" % job.unique)
2470 self.gearman_server.wakeConnections()
2471
2472 def getParameter(self, job, name):
2473 if isinstance(job, FakeBuild):
2474 return job.parameters[name]
2475 else:
2476 parameters = json.loads(job.arguments)
2477 return parameters[name]
2478
Clark Boylanb640e052014-04-03 16:41:46 -07002479 def haveAllBuildsReported(self):
2480 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002481 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002482 return False
2483 # Find out if every build that the worker has completed has been
2484 # reported back to Zuul. If it hasn't then that means a Gearman
2485 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002486 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002487 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002488 if not zbuild:
2489 # It has already been reported
2490 continue
2491 # It hasn't been reported yet.
2492 return False
2493 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002494 worker = self.executor_server.executor_worker
2495 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002496 if connection.state == 'GRAB_WAIT':
2497 return False
2498 return True
2499
2500 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002501 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002502 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002503 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002504 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002505 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002506 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002507 for j in conn.related_jobs.values():
2508 if j.unique == build.uuid:
2509 client_job = j
2510 break
2511 if not client_job:
2512 self.log.debug("%s is not known to the gearman client" %
2513 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002514 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002515 if not client_job.handle:
2516 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002517 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002518 server_job = self.gearman_server.jobs.get(client_job.handle)
2519 if not server_job:
2520 self.log.debug("%s is not known to the gearman server" %
2521 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002522 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002523 if not hasattr(server_job, 'waiting'):
2524 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002525 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002526 if server_job.waiting:
2527 continue
James E. Blair17302972016-08-10 16:11:42 -07002528 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002529 self.log.debug("%s has not reported start" % build)
2530 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002531 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002532 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002533 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002534 if worker_build:
2535 if worker_build.isWaiting():
2536 continue
2537 else:
2538 self.log.debug("%s is running" % worker_build)
2539 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002540 else:
James E. Blair962220f2016-08-03 11:22:38 -07002541 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002542 return False
James E. Blaira002b032017-04-18 10:35:48 -07002543 for (build_uuid, job_worker) in \
2544 self.executor_server.job_workers.items():
2545 if build_uuid not in seen_builds:
2546 self.log.debug("%s is not finalized" % build_uuid)
2547 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002548 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002549
James E. Blairdce6cea2016-12-20 16:45:32 -08002550 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002551 if self.fake_nodepool.paused:
2552 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002553 if self.sched.nodepool.requests:
2554 return False
2555 return True
2556
James E. Blaira615c362017-10-02 17:34:42 -07002557 def areAllMergeJobsWaiting(self):
2558 for client_job in list(self.merge_client.jobs):
2559 if not client_job.handle:
2560 self.log.debug("%s has no handle" % client_job)
2561 return False
2562 server_job = self.gearman_server.jobs.get(client_job.handle)
2563 if not server_job:
2564 self.log.debug("%s is not known to the gearman server" %
2565 client_job)
2566 return False
2567 if not hasattr(server_job, 'waiting'):
2568 self.log.debug("%s is being enqueued" % server_job)
2569 return False
2570 if server_job.waiting:
2571 self.log.debug("%s is waiting" % server_job)
2572 continue
2573 self.log.debug("%s is not waiting" % server_job)
2574 return False
2575 return True
2576
Jan Hruban6b71aff2015-10-22 16:58:08 +02002577 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002578 for event_queue in self.event_queues:
2579 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002580
2581 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002582 for event_queue in self.event_queues:
2583 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002584
Clark Boylanb640e052014-04-03 16:41:46 -07002585 def waitUntilSettled(self):
2586 self.log.debug("Waiting until settled...")
2587 start = time.time()
2588 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002589 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002590 self.log.error("Timeout waiting for Zuul to settle")
2591 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002592 for event_queue in self.event_queues:
2593 self.log.error(" %s: %s" %
2594 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002595 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002596 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002597 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002598 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002599 self.log.error("All requests completed: %s" %
2600 (self.areAllNodeRequestsComplete(),))
2601 self.log.error("Merge client jobs: %s" %
2602 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002603 raise Exception("Timeout waiting for Zuul to settle")
2604 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002605
Paul Belanger174a8272017-03-14 13:20:10 -04002606 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002607 # have all build states propogated to zuul?
2608 if self.haveAllBuildsReported():
2609 # Join ensures that the queue is empty _and_ events have been
2610 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002611 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002612 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002613 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002614 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002615 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002616 self.areAllNodeRequestsComplete() and
2617 all(self.eventQueuesEmpty())):
2618 # The queue empty check is placed at the end to
2619 # ensure that if a component adds an event between
2620 # when locked the run handler and checked that the
2621 # components were stable, we don't erroneously
2622 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002623 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002624 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002625 self.log.debug("...settled.")
2626 return
2627 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002628 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002629 self.sched.wake_event.wait(0.1)
2630
2631 def countJobResults(self, jobs, result):
2632 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002633 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002634
Monty Taylor0d926122017-05-24 08:07:56 -05002635 def getBuildByName(self, name):
2636 for build in self.builds:
2637 if build.name == name:
2638 return build
2639 raise Exception("Unable to find build %s" % name)
2640
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002641 def assertJobNotInHistory(self, name, project=None):
2642 for job in self.history:
2643 if (project is None or
2644 job.parameters['zuul']['project']['name'] == project):
2645 self.assertNotEqual(job.name, name,
2646 'Job %s found in history' % name)
2647
James E. Blair96c6bf82016-01-15 16:20:40 -08002648 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002649 for job in self.history:
2650 if (job.name == name and
2651 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002652 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002653 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002654 raise Exception("Unable to find job %s in history" % name)
2655
2656 def assertEmptyQueues(self):
2657 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002658 for tenant in self.sched.abide.tenants.values():
2659 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002660 for pipeline_queue in pipeline.queues:
2661 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002662 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002663 pipeline.name, pipeline_queue.name,
2664 pipeline_queue.queue))
2665 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002666 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002667
2668 def assertReportedStat(self, key, value=None, kind=None):
2669 start = time.time()
2670 while time.time() < (start + 5):
2671 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002672 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002673 if key == k:
2674 if value is None and kind is None:
2675 return
2676 elif value:
2677 if value == v:
2678 return
2679 elif kind:
2680 if v.endswith('|' + kind):
2681 return
2682 time.sleep(0.1)
2683
Clark Boylanb640e052014-04-03 16:41:46 -07002684 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002685
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002686 def assertBuilds(self, builds):
2687 """Assert that the running builds are as described.
2688
2689 The list of running builds is examined and must match exactly
2690 the list of builds described by the input.
2691
2692 :arg list builds: A list of dictionaries. Each item in the
2693 list must match the corresponding build in the build
2694 history, and each element of the dictionary must match the
2695 corresponding attribute of the build.
2696
2697 """
James E. Blair3158e282016-08-19 09:34:11 -07002698 try:
2699 self.assertEqual(len(self.builds), len(builds))
2700 for i, d in enumerate(builds):
2701 for k, v in d.items():
2702 self.assertEqual(
2703 getattr(self.builds[i], k), v,
2704 "Element %i in builds does not match" % (i,))
2705 except Exception:
2706 for build in self.builds:
2707 self.log.error("Running build: %s" % build)
2708 else:
2709 self.log.error("No running builds")
2710 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002711
James E. Blairb536ecc2016-08-31 10:11:42 -07002712 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002713 """Assert that the completed builds are as described.
2714
2715 The list of completed builds is examined and must match
2716 exactly the list of builds described by the input.
2717
2718 :arg list history: A list of dictionaries. Each item in the
2719 list must match the corresponding build in the build
2720 history, and each element of the dictionary must match the
2721 corresponding attribute of the build.
2722
James E. Blairb536ecc2016-08-31 10:11:42 -07002723 :arg bool ordered: If true, the history must match the order
2724 supplied, if false, the builds are permitted to have
2725 arrived in any order.
2726
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002727 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002728 def matches(history_item, item):
2729 for k, v in item.items():
2730 if getattr(history_item, k) != v:
2731 return False
2732 return True
James E. Blair3158e282016-08-19 09:34:11 -07002733 try:
2734 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002735 if ordered:
2736 for i, d in enumerate(history):
2737 if not matches(self.history[i], d):
2738 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002739 "Element %i in history does not match %s" %
2740 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002741 else:
2742 unseen = self.history[:]
2743 for i, d in enumerate(history):
2744 found = False
2745 for unseen_item in unseen:
2746 if matches(unseen_item, d):
2747 found = True
2748 unseen.remove(unseen_item)
2749 break
2750 if not found:
2751 raise Exception("No match found for element %i "
2752 "in history" % (i,))
2753 if unseen:
2754 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002755 except Exception:
2756 for build in self.history:
2757 self.log.error("Completed build: %s" % build)
2758 else:
2759 self.log.error("No completed builds")
2760 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002761
James E. Blair6ac368c2016-12-22 18:07:20 -08002762 def printHistory(self):
2763 """Log the build history.
2764
2765 This can be useful during tests to summarize what jobs have
2766 completed.
2767
2768 """
2769 self.log.debug("Build history:")
2770 for build in self.history:
2771 self.log.debug(build)
2772
James E. Blair59fdbac2015-12-07 17:08:06 -08002773 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002774 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2775
James E. Blair9ea70072017-04-19 16:05:30 -07002776 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002777 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002778 if not os.path.exists(root):
2779 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002780 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2781 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002782- tenant:
2783 name: openstack
2784 source:
2785 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002786 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002787 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002788 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002789 - org/project
2790 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002791 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002792 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002793 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002794 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002795 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002796
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002797 def addCommitToRepo(self, project, message, files,
2798 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002799 path = os.path.join(self.upstream_root, project)
2800 repo = git.Repo(path)
2801 repo.head.reference = branch
2802 zuul.merger.merger.reset_repo_to_head(repo)
2803 for fn, content in files.items():
2804 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002805 try:
2806 os.makedirs(os.path.dirname(fn))
2807 except OSError:
2808 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002809 with open(fn, 'w') as f:
2810 f.write(content)
2811 repo.index.add([fn])
2812 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002813 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002814 repo.heads[branch].commit = commit
2815 repo.head.reference = branch
2816 repo.git.clean('-x', '-f', '-d')
2817 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002818 if tag:
2819 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002820 return before
2821
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002822 def commitConfigUpdate(self, project_name, source_name):
2823 """Commit an update to zuul.yaml
2824
2825 This overwrites the zuul.yaml in the specificed project with
2826 the contents specified.
2827
2828 :arg str project_name: The name of the project containing
2829 zuul.yaml (e.g., common-config)
2830
2831 :arg str source_name: The path to the file (underneath the
2832 test fixture directory) whose contents should be used to
2833 replace zuul.yaml.
2834 """
2835
2836 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002837 files = {}
2838 with open(source_path, 'r') as f:
2839 data = f.read()
2840 layout = yaml.safe_load(data)
2841 files['zuul.yaml'] = data
2842 for item in layout:
2843 if 'job' in item:
2844 jobname = item['job']['name']
2845 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002846 before = self.addCommitToRepo(
2847 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002848 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002849 return before
2850
Clint Byrum627ba362017-08-14 13:20:40 -07002851 def newTenantConfig(self, source_name):
2852 """ Use this to update the tenant config file in tests
2853
2854 This will update self.tenant_config_file to point to a temporary file
2855 for the duration of this particular test. The content of that file will
2856 be taken from FIXTURE_DIR/source_name
2857
2858 After the test the original value of self.tenant_config_file will be
2859 restored.
2860
2861 :arg str source_name: The path of the file under
2862 FIXTURE_DIR that will be used to populate the new tenant
2863 config file.
2864 """
2865 source_path = os.path.join(FIXTURE_DIR, source_name)
2866 orig_tenant_config_file = self.tenant_config_file
2867 with tempfile.NamedTemporaryFile(
2868 delete=False, mode='wb') as new_tenant_config:
2869 self.tenant_config_file = new_tenant_config.name
2870 with open(source_path, mode='rb') as source_tenant_config:
2871 new_tenant_config.write(source_tenant_config.read())
2872 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2873 self.setupAllProjectKeys()
2874 self.log.debug(
2875 'tenant_config_file = {}'.format(self.tenant_config_file))
2876
2877 def _restoreTenantConfig():
2878 self.log.debug(
2879 'restoring tenant_config_file = {}'.format(
2880 orig_tenant_config_file))
2881 os.unlink(self.tenant_config_file)
2882 self.tenant_config_file = orig_tenant_config_file
2883 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2884 self.addCleanup(_restoreTenantConfig)
2885
James E. Blair7fc8daa2016-08-08 15:37:15 -07002886 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002887
James E. Blair7fc8daa2016-08-08 15:37:15 -07002888 """Inject a Fake (Gerrit) event.
2889
2890 This method accepts a JSON-encoded event and simulates Zuul
2891 having received it from Gerrit. It could (and should)
2892 eventually apply to any connection type, but is currently only
2893 used with Gerrit connections. The name of the connection is
2894 used to look up the corresponding server, and the event is
2895 simulated as having been received by all Zuul connections
2896 attached to that server. So if two Gerrit connections in Zuul
2897 are connected to the same Gerrit server, and you invoke this
2898 method specifying the name of one of them, the event will be
2899 received by both.
2900
2901 .. note::
2902
2903 "self.fake_gerrit.addEvent" calls should be migrated to
2904 this method.
2905
2906 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002907 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002908 :arg str event: The JSON-encoded event.
2909
2910 """
2911 specified_conn = self.connections.connections[connection]
2912 for conn in self.connections.connections.values():
2913 if (isinstance(conn, specified_conn.__class__) and
2914 specified_conn.server == conn.server):
2915 conn.addEvent(event)
2916
James E. Blaird8af5422017-05-24 13:59:40 -07002917 def getUpstreamRepos(self, projects):
2918 """Return upstream git repo objects for the listed projects
2919
2920 :arg list projects: A list of strings, each the canonical name
2921 of a project.
2922
2923 :returns: A dictionary of {name: repo} for every listed
2924 project.
2925 :rtype: dict
2926
2927 """
2928
2929 repos = {}
2930 for project in projects:
2931 # FIXME(jeblair): the upstream root does not yet have a
2932 # hostname component; that needs to be added, and this
2933 # line removed:
2934 tmp_project_name = '/'.join(project.split('/')[1:])
2935 path = os.path.join(self.upstream_root, tmp_project_name)
2936 repo = git.Repo(path)
2937 repos[project] = repo
2938 return repos
2939
James E. Blair3f876d52016-07-22 13:07:14 -07002940
2941class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002942 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07002943 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11002944
Jamie Lennox7655b552017-03-17 12:33:38 +11002945 @contextmanager
2946 def jobLog(self, build):
2947 """Print job logs on assertion errors
2948
2949 This method is a context manager which, if it encounters an
2950 ecxeption, adds the build log to the debug output.
2951
2952 :arg Build build: The build that's being asserted.
2953 """
2954 try:
2955 yield
2956 except Exception:
2957 path = os.path.join(self.test_root, build.uuid,
2958 'work', 'logs', 'job-output.txt')
2959 with open(path) as f:
2960 self.log.debug(f.read())
2961 raise
2962
Joshua Heskethd78b4482015-09-14 16:56:34 -06002963
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002964class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04002965 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002966 use_ssl = True
2967
2968
Joshua Heskethd78b4482015-09-14 16:56:34 -06002969class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08002970 def setup_config(self):
2971 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06002972 for section_name in self.config.sections():
2973 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
2974 section_name, re.I)
2975 if not con_match:
2976 continue
2977
2978 if self.config.get(section_name, 'driver') == 'sql':
2979 f = MySQLSchemaFixture()
2980 self.useFixture(f)
2981 if (self.config.get(section_name, 'dburi') ==
2982 '$MYSQL_FIXTURE_DBURI$'):
2983 self.config.set(section_name, 'dburi', f.dburi)