blob: bf1270c2e8950067f00f704c4cca5d6203a6e716 [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 Taylor4a781a72017-07-25 07:28:04 -040018import asyncio
Monty Taylorb934c1a2017-06-16 19:31:47 -050019import configparser
Jamie Lennox7655b552017-03-17 12:33:38 +110020from contextlib import contextmanager
Adam Gandelmand81dd762017-02-09 15:15:49 -080021import datetime
Clark Boylanb640e052014-04-03 16:41:46 -070022import gc
23import hashlib
Monty Taylorb934c1a2017-06-16 19:31:47 -050024from io import StringIO
Clark Boylanb640e052014-04-03 16:41:46 -070025import json
26import logging
27import os
Monty Taylorb934c1a2017-06-16 19:31:47 -050028import queue
Clark Boylanb640e052014-04-03 16:41:46 -070029import random
30import re
31import select
32import shutil
33import socket
34import string
35import subprocess
James E. Blair1c236df2017-02-01 14:07:24 -080036import sys
James E. Blairf84026c2015-12-08 16:11:46 -080037import tempfile
Clark Boylanb640e052014-04-03 16:41:46 -070038import threading
Clark Boylan8208c192017-04-24 18:08:08 -070039import traceback
Clark Boylanb640e052014-04-03 16:41:46 -070040import time
Joshua Heskethd78b4482015-09-14 16:56:34 -060041import uuid
Monty Taylorb934c1a2017-06-16 19:31:47 -050042import urllib
Monty Taylor4a781a72017-07-25 07:28:04 -040043import socketserver
44import http.server
Joshua Heskethd78b4482015-09-14 16:56:34 -060045
Clark Boylanb640e052014-04-03 16:41:46 -070046import git
47import gear
48import fixtures
James E. Blair498059b2016-12-20 13:50:13 -080049import kazoo.client
James E. Blairdce6cea2016-12-20 16:45:32 -080050import kazoo.exceptions
Joshua Heskethd78b4482015-09-14 16:56:34 -060051import pymysql
Clark Boylanb640e052014-04-03 16:41:46 -070052import testtools
James E. Blair1c236df2017-02-01 14:07:24 -080053import testtools.content
54import testtools.content_type
Clint Byrum3343e3e2016-11-15 16:05:03 -080055from git.exc import NoSuchPathError
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +000056import yaml
Clark Boylanb640e052014-04-03 16:41:46 -070057
James E. Blair6bacffb2018-01-05 13:45:25 -080058import tests.fakegithub
James E. Blaire511d2f2016-12-08 15:22:26 -080059import zuul.driver.gerrit.gerritsource as gerritsource
60import zuul.driver.gerrit.gerritconnection as gerritconnection
Gregory Haynes4fc12542015-04-22 20:38:06 -070061import zuul.driver.github.githubconnection as githubconnection
Clark Boylanb640e052014-04-03 16:41:46 -070062import zuul.scheduler
Paul Belanger174a8272017-03-14 13:20:10 -040063import zuul.executor.server
64import zuul.executor.client
James E. Blair83005782015-12-11 14:46:03 -080065import zuul.lib.connections
Clark Boylanb640e052014-04-03 16:41:46 -070066import zuul.merger.client
James E. Blair879dafb2015-07-17 14:04:49 -070067import zuul.merger.merger
68import zuul.merger.server
Tobias Henkeld91b4d72017-05-23 15:43:40 +020069import zuul.model
James E. Blair8d692392016-04-08 17:47:58 -070070import zuul.nodepool
Jesse Keating80730e62017-09-14 15:35:11 -060071import zuul.rpcclient
James E. Blairdce6cea2016-12-20 16:45:32 -080072import zuul.zk
James E. Blairb09a0c52017-10-04 07:35:14 -070073import zuul.configloader
Jan Hruban49bff072015-11-03 11:45:46 +010074from zuul.exceptions import MergeFailure
Jesse Keating80730e62017-09-14 15:35:11 -060075from zuul.lib.config import get_default
Clark Boylanb640e052014-04-03 16:41:46 -070076
77FIXTURE_DIR = os.path.join(os.path.dirname(__file__),
78 'fixtures')
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -080079
80KEEP_TEMPDIRS = bool(os.environ.get('KEEP_TEMPDIRS', False))
Clark Boylanb640e052014-04-03 16:41:46 -070081
Clark Boylanb640e052014-04-03 16:41:46 -070082
83def repack_repo(path):
84 cmd = ['git', '--git-dir=%s/.git' % path, 'repack', '-afd']
85 output = subprocess.Popen(cmd, close_fds=True,
86 stdout=subprocess.PIPE,
87 stderr=subprocess.PIPE)
88 out = output.communicate()
89 if output.returncode:
90 raise Exception("git repack returned %d" % output.returncode)
91 return out
92
93
94def random_sha1():
Clint Byrumc0923d52017-05-10 15:47:41 -040095 return hashlib.sha1(str(random.random()).encode('ascii')).hexdigest()
Clark Boylanb640e052014-04-03 16:41:46 -070096
97
James E. Blaira190f3b2015-01-05 14:56:54 -080098def iterate_timeout(max_seconds, purpose):
99 start = time.time()
100 count = 0
101 while (time.time() < start + max_seconds):
102 count += 1
103 yield count
104 time.sleep(0)
105 raise Exception("Timeout waiting for %s" % purpose)
106
107
Jesse Keating436a5452017-04-20 11:48:41 -0700108def simple_layout(path, driver='gerrit'):
James E. Blair06cc3922017-04-19 10:08:10 -0700109 """Specify a layout file for use by a test method.
110
111 :arg str path: The path to the layout file.
Jesse Keating436a5452017-04-20 11:48:41 -0700112 :arg str driver: The source driver to use, defaults to gerrit.
James E. Blair06cc3922017-04-19 10:08:10 -0700113
114 Some tests require only a very simple configuration. For those,
115 establishing a complete config directory hierachy is too much
116 work. In those cases, you can add a simple zuul.yaml file to the
117 test fixtures directory (in fixtures/layouts/foo.yaml) and use
118 this decorator to indicate the test method should use that rather
119 than the tenant config file specified by the test class.
120
121 The decorator will cause that layout file to be added to a
122 config-project called "common-config" and each "project" instance
123 referenced in the layout file will have a git repo automatically
124 initialized.
125 """
126
127 def decorator(test):
Jesse Keating436a5452017-04-20 11:48:41 -0700128 test.__simple_layout__ = (path, driver)
James E. Blair06cc3922017-04-19 10:08:10 -0700129 return test
130 return decorator
131
132
Gregory Haynes4fc12542015-04-22 20:38:06 -0700133class GerritChangeReference(git.Reference):
Clark Boylanb640e052014-04-03 16:41:46 -0700134 _common_path_default = "refs/changes"
135 _points_to_commits_only = True
136
137
Gregory Haynes4fc12542015-04-22 20:38:06 -0700138class FakeGerritChange(object):
Tobias Henkelea98a192017-05-29 21:15:17 +0200139 categories = {'Approved': ('Approved', -1, 1),
140 'Code-Review': ('Code-Review', -2, 2),
141 'Verified': ('Verified', -2, 2)}
142
Clark Boylanb640e052014-04-03 16:41:46 -0700143 def __init__(self, gerrit, number, project, branch, subject,
James E. Blair289f5932017-07-27 15:02:29 -0700144 status='NEW', upstream_root=None, files={},
145 parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700146 self.gerrit = gerrit
Gregory Haynes4fc12542015-04-22 20:38:06 -0700147 self.source = gerrit
Clark Boylanb640e052014-04-03 16:41:46 -0700148 self.reported = 0
149 self.queried = 0
150 self.patchsets = []
151 self.number = number
152 self.project = project
153 self.branch = branch
154 self.subject = subject
155 self.latest_patchset = 0
156 self.depends_on_change = None
157 self.needed_by_changes = []
158 self.fail_merge = False
159 self.messages = []
160 self.data = {
161 'branch': branch,
162 'comments': [],
163 'commitMessage': subject,
164 'createdOn': time.time(),
165 'id': 'I' + random_sha1(),
166 'lastUpdated': time.time(),
167 'number': str(number),
168 'open': status == 'NEW',
169 'owner': {'email': 'user@example.com',
170 'name': 'User Name',
171 'username': 'username'},
172 'patchSets': self.patchsets,
173 'project': project,
174 'status': status,
175 'subject': subject,
176 'submitRecords': [],
James E. Blair0e4c7912018-01-02 14:20:17 -0800177 'url': 'https://%s/%s' % (self.gerrit.server, number)}
Clark Boylanb640e052014-04-03 16:41:46 -0700178
179 self.upstream_root = upstream_root
James E. Blair289f5932017-07-27 15:02:29 -0700180 self.addPatchset(files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700181 self.data['submitRecords'] = self.getSubmitRecords()
182 self.open = status == 'NEW'
183
James E. Blair289f5932017-07-27 15:02:29 -0700184 def addFakeChangeToRepo(self, msg, files, large, parent):
Clark Boylanb640e052014-04-03 16:41:46 -0700185 path = os.path.join(self.upstream_root, self.project)
186 repo = git.Repo(path)
James E. Blair289f5932017-07-27 15:02:29 -0700187 if parent is None:
188 parent = 'refs/tags/init'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700189 ref = GerritChangeReference.create(
190 repo, '1/%s/%s' % (self.number, self.latest_patchset),
James E. Blair289f5932017-07-27 15:02:29 -0700191 parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700192 repo.head.reference = ref
James E. Blair879dafb2015-07-17 14:04:49 -0700193 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700194 repo.git.clean('-x', '-f', '-d')
195
196 path = os.path.join(self.upstream_root, self.project)
197 if not large:
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700198 for fn, content in files.items():
199 fn = os.path.join(path, fn)
James E. Blair332636e2017-09-05 10:14:35 -0700200 if content is None:
201 os.unlink(fn)
202 repo.index.remove([fn])
203 else:
204 d = os.path.dirname(fn)
205 if not os.path.exists(d):
206 os.makedirs(d)
207 with open(fn, 'w') as f:
208 f.write(content)
209 repo.index.add([fn])
Clark Boylanb640e052014-04-03 16:41:46 -0700210 else:
211 for fni in range(100):
212 fn = os.path.join(path, str(fni))
213 f = open(fn, 'w')
214 for ci in range(4096):
215 f.write(random.choice(string.printable))
216 f.close()
217 repo.index.add([fn])
218
219 r = repo.index.commit(msg)
220 repo.head.reference = 'master'
James E. Blair879dafb2015-07-17 14:04:49 -0700221 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -0700222 repo.git.clean('-x', '-f', '-d')
223 repo.heads['master'].checkout()
224 return r
225
James E. Blair289f5932017-07-27 15:02:29 -0700226 def addPatchset(self, files=None, large=False, parent=None):
Clark Boylanb640e052014-04-03 16:41:46 -0700227 self.latest_patchset += 1
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700228 if not files:
James E. Blair97d902e2014-08-21 13:25:56 -0700229 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700230 data = ("test %s %s %s\n" %
231 (self.branch, self.number, self.latest_patchset))
232 files = {fn: data}
Clark Boylanb640e052014-04-03 16:41:46 -0700233 msg = self.subject + '-' + str(self.latest_patchset)
James E. Blair289f5932017-07-27 15:02:29 -0700234 c = self.addFakeChangeToRepo(msg, files, large, parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700235 ps_files = [{'file': '/COMMIT_MSG',
236 'type': 'ADDED'},
237 {'file': 'README',
238 'type': 'MODIFIED'}]
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700239 for f in files.keys():
Clark Boylanb640e052014-04-03 16:41:46 -0700240 ps_files.append({'file': f, 'type': 'ADDED'})
241 d = {'approvals': [],
242 'createdOn': time.time(),
243 'files': ps_files,
244 'number': str(self.latest_patchset),
245 'ref': 'refs/changes/1/%s/%s' % (self.number,
246 self.latest_patchset),
247 'revision': c.hexsha,
248 'uploader': {'email': 'user@example.com',
249 'name': 'User name',
250 'username': 'user'}}
251 self.data['currentPatchSet'] = d
252 self.patchsets.append(d)
253 self.data['submitRecords'] = self.getSubmitRecords()
254
255 def getPatchsetCreatedEvent(self, patchset):
256 event = {"type": "patchset-created",
257 "change": {"project": self.project,
258 "branch": self.branch,
259 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
260 "number": str(self.number),
261 "subject": self.subject,
262 "owner": {"name": "User Name"},
263 "url": "https://hostname/3"},
264 "patchSet": self.patchsets[patchset - 1],
265 "uploader": {"name": "User Name"}}
266 return event
267
268 def getChangeRestoredEvent(self):
269 event = {"type": "change-restored",
270 "change": {"project": self.project,
271 "branch": self.branch,
272 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
273 "number": str(self.number),
274 "subject": self.subject,
275 "owner": {"name": "User Name"},
276 "url": "https://hostname/3"},
277 "restorer": {"name": "User Name"},
Antoine Mussobd86a312014-01-08 14:51:33 +0100278 "patchSet": self.patchsets[-1],
279 "reason": ""}
280 return event
281
282 def getChangeAbandonedEvent(self):
283 event = {"type": "change-abandoned",
284 "change": {"project": self.project,
285 "branch": self.branch,
286 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
287 "number": str(self.number),
288 "subject": self.subject,
289 "owner": {"name": "User Name"},
290 "url": "https://hostname/3"},
291 "abandoner": {"name": "User Name"},
292 "patchSet": self.patchsets[-1],
Clark Boylanb640e052014-04-03 16:41:46 -0700293 "reason": ""}
294 return event
295
296 def getChangeCommentEvent(self, patchset):
297 event = {"type": "comment-added",
298 "change": {"project": self.project,
299 "branch": self.branch,
300 "id": "I5459869c07352a31bfb1e7a8cac379cabfcb25af",
301 "number": str(self.number),
302 "subject": self.subject,
303 "owner": {"name": "User Name"},
304 "url": "https://hostname/3"},
305 "patchSet": self.patchsets[patchset - 1],
306 "author": {"name": "User Name"},
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200307 "approvals": [{"type": "Code-Review",
Clark Boylanb640e052014-04-03 16:41:46 -0700308 "description": "Code-Review",
309 "value": "0"}],
310 "comment": "This is a comment"}
311 return event
312
James E. Blairc2a5ed72017-02-20 14:12:01 -0500313 def getChangeMergedEvent(self):
314 event = {"submitter": {"name": "Jenkins",
315 "username": "jenkins"},
316 "newRev": "29ed3b5f8f750a225c5be70235230e3a6ccb04d9",
317 "patchSet": self.patchsets[-1],
318 "change": self.data,
319 "type": "change-merged",
320 "eventCreatedOn": 1487613810}
321 return event
322
James E. Blair8cce42e2016-10-18 08:18:36 -0700323 def getRefUpdatedEvent(self):
324 path = os.path.join(self.upstream_root, self.project)
325 repo = git.Repo(path)
326 oldrev = repo.heads[self.branch].commit.hexsha
327
328 event = {
329 "type": "ref-updated",
330 "submitter": {
331 "name": "User Name",
332 },
333 "refUpdate": {
334 "oldRev": oldrev,
335 "newRev": self.patchsets[-1]['revision'],
336 "refName": self.branch,
337 "project": self.project,
338 }
339 }
340 return event
341
Joshua Hesketh642824b2014-07-01 17:54:59 +1000342 def addApproval(self, category, value, username='reviewer_john',
343 granted_on=None, message=''):
Clark Boylanb640e052014-04-03 16:41:46 -0700344 if not granted_on:
345 granted_on = time.time()
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000346 approval = {
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200347 'description': self.categories[category][0],
348 'type': category,
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000349 'value': str(value),
350 'by': {
351 'username': username,
352 'email': username + '@example.com',
353 },
354 'grantedOn': int(granted_on)
355 }
Clark Boylanb640e052014-04-03 16:41:46 -0700356 for i, x in enumerate(self.patchsets[-1]['approvals'][:]):
Tobias Henkelbf24fd12017-07-27 06:13:07 +0200357 if x['by']['username'] == username and x['type'] == category:
Clark Boylanb640e052014-04-03 16:41:46 -0700358 del self.patchsets[-1]['approvals'][i]
359 self.patchsets[-1]['approvals'].append(approval)
360 event = {'approvals': [approval],
Joshua Hesketh642824b2014-07-01 17:54:59 +1000361 'author': {'email': 'author@example.com',
362 'name': 'Patchset Author',
363 'username': 'author_phil'},
Clark Boylanb640e052014-04-03 16:41:46 -0700364 'change': {'branch': self.branch,
365 'id': 'Iaa69c46accf97d0598111724a38250ae76a22c87',
366 'number': str(self.number),
Joshua Hesketh642824b2014-07-01 17:54:59 +1000367 'owner': {'email': 'owner@example.com',
368 'name': 'Change Owner',
369 'username': 'owner_jane'},
Clark Boylanb640e052014-04-03 16:41:46 -0700370 'project': self.project,
371 'subject': self.subject,
372 'topic': 'master',
373 'url': 'https://hostname/459'},
Joshua Hesketh642824b2014-07-01 17:54:59 +1000374 'comment': message,
Clark Boylanb640e052014-04-03 16:41:46 -0700375 'patchSet': self.patchsets[-1],
376 'type': 'comment-added'}
377 self.data['submitRecords'] = self.getSubmitRecords()
378 return json.loads(json.dumps(event))
379
380 def getSubmitRecords(self):
381 status = {}
382 for cat in self.categories.keys():
383 status[cat] = 0
384
385 for a in self.patchsets[-1]['approvals']:
386 cur = status[a['type']]
387 cat_min, cat_max = self.categories[a['type']][1:]
388 new = int(a['value'])
389 if new == cat_min:
390 cur = new
391 elif abs(new) > abs(cur):
392 cur = new
393 status[a['type']] = cur
394
395 labels = []
396 ok = True
397 for typ, cat in self.categories.items():
398 cur = status[typ]
399 cat_min, cat_max = cat[1:]
400 if cur == cat_min:
401 value = 'REJECT'
402 ok = False
403 elif cur == cat_max:
404 value = 'OK'
405 else:
406 value = 'NEED'
407 ok = False
408 labels.append({'label': cat[0], 'status': value})
409 if ok:
410 return [{'status': 'OK'}]
411 return [{'status': 'NOT_READY',
412 'labels': labels}]
413
414 def setDependsOn(self, other, patchset):
415 self.depends_on_change = other
416 d = {'id': other.data['id'],
417 'number': other.data['number'],
418 'ref': other.patchsets[patchset - 1]['ref']
419 }
420 self.data['dependsOn'] = [d]
421
422 other.needed_by_changes.append(self)
423 needed = other.data.get('neededBy', [])
424 d = {'id': self.data['id'],
425 'number': self.data['number'],
James E. Blairdb93b302017-07-19 15:33:11 -0700426 'ref': self.patchsets[-1]['ref'],
427 'revision': self.patchsets[-1]['revision']
Clark Boylanb640e052014-04-03 16:41:46 -0700428 }
429 needed.append(d)
430 other.data['neededBy'] = needed
431
432 def query(self):
433 self.queried += 1
434 d = self.data.get('dependsOn')
435 if d:
436 d = d[0]
437 if (self.depends_on_change.patchsets[-1]['ref'] == d['ref']):
438 d['isCurrentPatchSet'] = True
439 else:
440 d['isCurrentPatchSet'] = False
441 return json.loads(json.dumps(self.data))
442
443 def setMerged(self):
444 if (self.depends_on_change and
Joshua Hesketh29d99b72014-08-19 16:27:42 +1000445 self.depends_on_change.data['status'] != 'MERGED'):
Clark Boylanb640e052014-04-03 16:41:46 -0700446 return
447 if self.fail_merge:
448 return
449 self.data['status'] = 'MERGED'
450 self.open = False
451
452 path = os.path.join(self.upstream_root, self.project)
453 repo = git.Repo(path)
454 repo.heads[self.branch].commit = \
455 repo.commit(self.patchsets[-1]['revision'])
456
457 def setReported(self):
458 self.reported += 1
459
460
James E. Blaire511d2f2016-12-08 15:22:26 -0800461class FakeGerritConnection(gerritconnection.GerritConnection):
James E. Blaire7b99a02016-08-05 14:27:34 -0700462 """A Fake Gerrit connection for use in tests.
463
464 This subclasses
465 :py:class:`~zuul.connection.gerrit.GerritConnection` to add the
466 ability for tests to add changes to the fake Gerrit it represents.
467 """
468
Joshua Hesketh352264b2015-08-11 23:42:08 +1000469 log = logging.getLogger("zuul.test.FakeGerritConnection")
James E. Blair96698e22015-04-02 07:48:21 -0700470
James E. Blaire511d2f2016-12-08 15:22:26 -0800471 def __init__(self, driver, connection_name, connection_config,
James E. Blair7fc8daa2016-08-08 15:37:15 -0700472 changes_db=None, upstream_root=None):
James E. Blaire511d2f2016-12-08 15:22:26 -0800473 super(FakeGerritConnection, self).__init__(driver, connection_name,
Joshua Hesketh352264b2015-08-11 23:42:08 +1000474 connection_config)
475
Monty Taylorb934c1a2017-06-16 19:31:47 -0500476 self.event_queue = queue.Queue()
Clark Boylanb640e052014-04-03 16:41:46 -0700477 self.fixture_dir = os.path.join(FIXTURE_DIR, 'gerrit')
478 self.change_number = 0
Joshua Hesketh352264b2015-08-11 23:42:08 +1000479 self.changes = changes_db
James E. Blairf8ff9932014-08-15 15:24:24 -0700480 self.queries = []
Jan Hruban6b71aff2015-10-22 16:58:08 +0200481 self.upstream_root = upstream_root
Clark Boylanb640e052014-04-03 16:41:46 -0700482
James E. Blair8b1dc3f2016-07-05 16:49:00 -0700483 def addFakeChange(self, project, branch, subject, status='NEW',
James E. Blair289f5932017-07-27 15:02:29 -0700484 files=None, parent=None):
James E. Blaire7b99a02016-08-05 14:27:34 -0700485 """Add a change to the fake Gerrit."""
Clark Boylanb640e052014-04-03 16:41:46 -0700486 self.change_number += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700487 c = FakeGerritChange(self, self.change_number, project, branch,
488 subject, upstream_root=self.upstream_root,
James E. Blair289f5932017-07-27 15:02:29 -0700489 status=status, files=files, parent=parent)
Clark Boylanb640e052014-04-03 16:41:46 -0700490 self.changes[self.change_number] = c
491 return c
492
James E. Blair1edfd972017-12-01 15:54:24 -0800493 def addFakeTag(self, project, branch, tag):
494 path = os.path.join(self.upstream_root, project)
495 repo = git.Repo(path)
496 commit = repo.heads[branch].commit
497 newrev = commit.hexsha
498 ref = 'refs/tags/' + tag
499
500 git.Tag.create(repo, tag, commit)
501
502 event = {
503 "type": "ref-updated",
504 "submitter": {
505 "name": "User Name",
506 },
507 "refUpdate": {
508 "oldRev": 40 * '0',
509 "newRev": newrev,
510 "refName": ref,
511 "project": project,
512 }
513 }
514 return event
515
James E. Blair72facdc2017-08-17 10:29:12 -0700516 def getFakeBranchCreatedEvent(self, project, branch):
517 path = os.path.join(self.upstream_root, project)
518 repo = git.Repo(path)
519 oldrev = 40 * '0'
520
521 event = {
522 "type": "ref-updated",
523 "submitter": {
524 "name": "User Name",
525 },
526 "refUpdate": {
527 "oldRev": oldrev,
528 "newRev": repo.heads[branch].commit.hexsha,
James E. Blair24690ec2017-11-02 09:05:01 -0700529 "refName": 'refs/heads/' + branch,
James E. Blair72facdc2017-08-17 10:29:12 -0700530 "project": project,
531 }
532 }
533 return event
534
James E. Blairc6d48652018-02-14 14:20:13 -0800535 def getFakeBranchDeletedEvent(self, project, branch):
536 oldrev = '4abd38457c2da2a72d4d030219ab180ecdb04bf0'
537 newrev = 40 * '0'
538
539 event = {
540 "type": "ref-updated",
541 "submitter": {
542 "name": "User Name",
543 },
544 "refUpdate": {
545 "oldRev": oldrev,
546 "newRev": newrev,
547 "refName": 'refs/heads/' + branch,
548 "project": project,
549 }
550 }
551 return event
552
Clark Boylanb640e052014-04-03 16:41:46 -0700553 def review(self, project, changeid, message, action):
554 number, ps = changeid.split(',')
555 change = self.changes[int(number)]
Joshua Hesketh642824b2014-07-01 17:54:59 +1000556
557 # Add the approval back onto the change (ie simulate what gerrit would
558 # do).
559 # Usually when zuul leaves a review it'll create a feedback loop where
560 # zuul's review enters another gerrit event (which is then picked up by
561 # zuul). However, we can't mimic this behaviour (by adding this
562 # approval event into the queue) as it stops jobs from checking what
563 # happens before this event is triggered. If a job needs to see what
564 # happens they can add their own verified event into the queue.
565 # Nevertheless, we can update change with the new review in gerrit.
566
James E. Blair8b5408c2016-08-08 15:37:46 -0700567 for cat in action.keys():
568 if cat != 'submit':
Joshua Hesketh352264b2015-08-11 23:42:08 +1000569 change.addApproval(cat, action[cat], username=self.user)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000570
Clark Boylanb640e052014-04-03 16:41:46 -0700571 change.messages.append(message)
Joshua Hesketh642824b2014-07-01 17:54:59 +1000572
Clark Boylanb640e052014-04-03 16:41:46 -0700573 if 'submit' in action:
574 change.setMerged()
575 if message:
576 change.setReported()
577
578 def query(self, number):
579 change = self.changes.get(int(number))
580 if change:
581 return change.query()
582 return {}
583
James E. Blair0e4c7912018-01-02 14:20:17 -0800584 def _simpleQuery(self, query):
James E. Blair5ee24252014-12-30 10:12:29 -0800585 if query.startswith('change:'):
586 # Query a specific changeid
587 changeid = query[len('change:'):]
588 l = [change.query() for change in self.changes.values()
James E. Blair0e4c7912018-01-02 14:20:17 -0800589 if (change.data['id'] == changeid or
590 change.data['number'] == changeid)]
James E. Blair96698e22015-04-02 07:48:21 -0700591 elif query.startswith('message:'):
592 # Query the content of a commit message
593 msg = query[len('message:'):].strip()
594 l = [change.query() for change in self.changes.values()
595 if msg in change.data['commitMessage']]
James E. Blair5ee24252014-12-30 10:12:29 -0800596 else:
597 # Query all open changes
598 l = [change.query() for change in self.changes.values()]
James E. Blairf8ff9932014-08-15 15:24:24 -0700599 return l
James E. Blairc494d542014-08-06 09:23:52 -0700600
James E. Blair0e4c7912018-01-02 14:20:17 -0800601 def simpleQuery(self, query):
602 self.log.debug("simpleQuery: %s" % query)
603 self.queries.append(query)
604 results = []
605 if query.startswith('(') and 'OR' in query:
606 query = query[1:-2]
607 for q in query.split(' OR '):
608 for r in self._simpleQuery(q):
609 if r not in results:
610 results.append(r)
611 else:
612 results = self._simpleQuery(query)
613 return results
614
Joshua Hesketh352264b2015-08-11 23:42:08 +1000615 def _start_watcher_thread(self, *args, **kw):
Clark Boylanb640e052014-04-03 16:41:46 -0700616 pass
617
Tobias Henkeld91b4d72017-05-23 15:43:40 +0200618 def _uploadPack(self, project):
619 ret = ('00a31270149696713ba7e06f1beb760f20d359c4abed HEAD\x00'
620 'multi_ack thin-pack side-band side-band-64k ofs-delta '
621 'shallow no-progress include-tag multi_ack_detailed no-done\n')
622 path = os.path.join(self.upstream_root, project.name)
623 repo = git.Repo(path)
624 for ref in repo.refs:
625 r = ref.object.hexsha + ' ' + ref.path + '\n'
626 ret += '%04x%s' % (len(r) + 4, r)
627 ret += '0000'
628 return ret
629
Joshua Hesketh352264b2015-08-11 23:42:08 +1000630 def getGitUrl(self, project):
James E. Blairda5bb7e2018-01-22 16:12:17 -0800631 return 'file://' + os.path.join(self.upstream_root, project.name)
Joshua Hesketh352264b2015-08-11 23:42:08 +1000632
Clark Boylanb640e052014-04-03 16:41:46 -0700633
Gregory Haynes4fc12542015-04-22 20:38:06 -0700634class GithubChangeReference(git.Reference):
635 _common_path_default = "refs/pull"
636 _points_to_commits_only = True
637
638
639class FakeGithubPullRequest(object):
640
641 def __init__(self, github, number, project, branch,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800642 subject, upstream_root, files=[], number_of_commits=1,
Jesse Keating152a4022017-07-07 08:39:52 -0700643 writers=[], body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700644 """Creates a new PR with several commits.
645 Sends an event about opened PR."""
646 self.github = github
647 self.source = github
648 self.number = number
649 self.project = project
650 self.branch = branch
Jan Hruban37615e52015-11-19 14:30:49 +0100651 self.subject = subject
Jesse Keatinga41566f2017-06-14 18:17:51 -0700652 self.body = body
Jan Hruban37615e52015-11-19 14:30:49 +0100653 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700654 self.upstream_root = upstream_root
Jan Hruban570d01c2016-03-10 21:51:32 +0100655 self.files = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700656 self.comments = []
Jan Hruban16ad31f2015-11-07 14:39:07 +0100657 self.labels = []
Jan Hrubane252a732017-01-03 15:03:09 +0100658 self.statuses = {}
Jesse Keatingae4cd272017-01-30 17:10:44 -0800659 self.reviews = []
660 self.writers = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700661 self.updated_at = None
662 self.head_sha = None
Jan Hruban49bff072015-11-03 11:45:46 +0100663 self.is_merged = False
Jan Hruban3b415922016-02-03 13:10:22 +0100664 self.merge_message = None
Jesse Keating4a27f132017-05-25 16:44:01 -0700665 self.state = 'open'
James E. Blair54145e02018-01-10 16:07:41 -0800666 self.url = 'https://%s/%s/pull/%s' % (github.server, project, number)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700667 self._createPRRef()
Jan Hruban570d01c2016-03-10 21:51:32 +0100668 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700669 self._updateTimeStamp()
670
Jan Hruban570d01c2016-03-10 21:51:32 +0100671 def addCommit(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700672 """Adds a commit on top of the actual PR head."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100673 self._addCommitToRepo(files=files)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700674 self._updateTimeStamp()
675
Jan Hruban570d01c2016-03-10 21:51:32 +0100676 def forcePush(self, files=[]):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700677 """Clears actual commits and add a commit on top of the base."""
Jan Hruban570d01c2016-03-10 21:51:32 +0100678 self._addCommitToRepo(files=files, reset=True)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700679 self._updateTimeStamp()
680
681 def getPullRequestOpenedEvent(self):
682 return self._getPullRequestEvent('opened')
683
684 def getPullRequestSynchronizeEvent(self):
685 return self._getPullRequestEvent('synchronize')
686
687 def getPullRequestReopenedEvent(self):
688 return self._getPullRequestEvent('reopened')
689
690 def getPullRequestClosedEvent(self):
691 return self._getPullRequestEvent('closed')
692
Jesse Keatinga41566f2017-06-14 18:17:51 -0700693 def getPullRequestEditedEvent(self):
694 return self._getPullRequestEvent('edited')
695
Gregory Haynes4fc12542015-04-22 20:38:06 -0700696 def addComment(self, message):
697 self.comments.append(message)
698 self._updateTimeStamp()
699
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200700 def getCommentAddedEvent(self, text):
701 name = 'issue_comment'
702 data = {
703 'action': 'created',
704 'issue': {
705 'number': self.number
706 },
707 'comment': {
708 'body': text
709 },
710 'repository': {
711 'full_name': self.project
Jan Hruban3b415922016-02-03 13:10:22 +0100712 },
713 'sender': {
714 'login': 'ghuser'
Jan Hrubanc7ab1602015-10-14 15:29:33 +0200715 }
716 }
717 return (name, data)
718
Jesse Keating5c05a9f2017-01-12 14:44:58 -0800719 def getReviewAddedEvent(self, review):
720 name = 'pull_request_review'
721 data = {
722 'action': 'submitted',
723 'pull_request': {
724 'number': self.number,
725 'title': self.subject,
726 'updated_at': self.updated_at,
727 'base': {
728 'ref': self.branch,
729 'repo': {
730 'full_name': self.project
731 }
732 },
733 'head': {
734 'sha': self.head_sha
735 }
736 },
737 'review': {
738 'state': review
739 },
740 'repository': {
741 'full_name': self.project
742 },
743 'sender': {
744 'login': 'ghuser'
745 }
746 }
747 return (name, data)
748
Jan Hruban16ad31f2015-11-07 14:39:07 +0100749 def addLabel(self, name):
750 if name not in self.labels:
751 self.labels.append(name)
752 self._updateTimeStamp()
753 return self._getLabelEvent(name)
754
755 def removeLabel(self, name):
756 if name in self.labels:
757 self.labels.remove(name)
758 self._updateTimeStamp()
759 return self._getUnlabelEvent(name)
760
761 def _getLabelEvent(self, label):
762 name = 'pull_request'
763 data = {
764 'action': 'labeled',
765 'pull_request': {
766 'number': self.number,
767 'updated_at': self.updated_at,
768 'base': {
769 'ref': self.branch,
770 'repo': {
771 'full_name': self.project
772 }
773 },
774 'head': {
775 'sha': self.head_sha
776 }
777 },
778 'label': {
779 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100780 },
781 'sender': {
782 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100783 }
784 }
785 return (name, data)
786
787 def _getUnlabelEvent(self, label):
788 name = 'pull_request'
789 data = {
790 'action': 'unlabeled',
791 'pull_request': {
792 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100793 'title': self.subject,
Jan Hruban16ad31f2015-11-07 14:39:07 +0100794 'updated_at': self.updated_at,
795 'base': {
796 'ref': self.branch,
797 'repo': {
798 'full_name': self.project
799 }
800 },
801 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800802 'sha': self.head_sha,
803 'repo': {
804 'full_name': self.project
805 }
Jan Hruban16ad31f2015-11-07 14:39:07 +0100806 }
807 },
808 'label': {
809 'name': label
Jan Hruban3b415922016-02-03 13:10:22 +0100810 },
811 'sender': {
812 'login': 'ghuser'
Jan Hruban16ad31f2015-11-07 14:39:07 +0100813 }
814 }
815 return (name, data)
816
Jesse Keatinga41566f2017-06-14 18:17:51 -0700817 def editBody(self, body):
818 self.body = body
819 self._updateTimeStamp()
820
Gregory Haynes4fc12542015-04-22 20:38:06 -0700821 def _getRepo(self):
822 repo_path = os.path.join(self.upstream_root, self.project)
823 return git.Repo(repo_path)
824
825 def _createPRRef(self):
826 repo = self._getRepo()
827 GithubChangeReference.create(
828 repo, self._getPRReference(), 'refs/tags/init')
829
Jan Hruban570d01c2016-03-10 21:51:32 +0100830 def _addCommitToRepo(self, files=[], reset=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700831 repo = self._getRepo()
832 ref = repo.references[self._getPRReference()]
833 if reset:
Jan Hruban37615e52015-11-19 14:30:49 +0100834 self.number_of_commits = 0
Gregory Haynes4fc12542015-04-22 20:38:06 -0700835 ref.set_object('refs/tags/init')
Jan Hruban37615e52015-11-19 14:30:49 +0100836 self.number_of_commits += 1
Gregory Haynes4fc12542015-04-22 20:38:06 -0700837 repo.head.reference = ref
838 zuul.merger.merger.reset_repo_to_head(repo)
839 repo.git.clean('-x', '-f', '-d')
840
Jan Hruban570d01c2016-03-10 21:51:32 +0100841 if files:
842 fn = files[0]
843 self.files = files
844 else:
845 fn = '%s-%s' % (self.branch.replace('/', '_'), self.number)
846 self.files = [fn]
Jan Hruban37615e52015-11-19 14:30:49 +0100847 msg = self.subject + '-' + str(self.number_of_commits)
Gregory Haynes4fc12542015-04-22 20:38:06 -0700848 fn = os.path.join(repo.working_dir, fn)
849 f = open(fn, 'w')
850 with open(fn, 'w') as f:
851 f.write("test %s %s\n" %
852 (self.branch, self.number))
853 repo.index.add([fn])
854
855 self.head_sha = repo.index.commit(msg).hexsha
Jesse Keatingd96e5882017-01-19 13:55:50 -0800856 # Create an empty set of statuses for the given sha,
857 # each sha on a PR may have a status set on it
858 self.statuses[self.head_sha] = []
Gregory Haynes4fc12542015-04-22 20:38:06 -0700859 repo.head.reference = 'master'
860 zuul.merger.merger.reset_repo_to_head(repo)
861 repo.git.clean('-x', '-f', '-d')
862 repo.heads['master'].checkout()
863
864 def _updateTimeStamp(self):
865 self.updated_at = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.localtime())
866
867 def getPRHeadSha(self):
868 repo = self._getRepo()
869 return repo.references[self._getPRReference()].commit.hexsha
870
Jesse Keatingae4cd272017-01-30 17:10:44 -0800871 def addReview(self, user, state, granted_on=None):
Adam Gandelmand81dd762017-02-09 15:15:49 -0800872 gh_time_format = '%Y-%m-%dT%H:%M:%SZ'
873 # convert the timestamp to a str format that would be returned
874 # from github as 'submitted_at' in the API response
Jesse Keatingae4cd272017-01-30 17:10:44 -0800875
Adam Gandelmand81dd762017-02-09 15:15:49 -0800876 if granted_on:
877 granted_on = datetime.datetime.utcfromtimestamp(granted_on)
878 submitted_at = time.strftime(
879 gh_time_format, granted_on.timetuple())
880 else:
881 # github timestamps only down to the second, so we need to make
882 # sure reviews that tests add appear to be added over a period of
883 # time in the past and not all at once.
884 if not self.reviews:
885 # the first review happens 10 mins ago
886 offset = 600
887 else:
888 # subsequent reviews happen 1 minute closer to now
889 offset = 600 - (len(self.reviews) * 60)
890
891 granted_on = datetime.datetime.utcfromtimestamp(
892 time.time() - offset)
893 submitted_at = time.strftime(
894 gh_time_format, granted_on.timetuple())
895
Jesse Keatingae4cd272017-01-30 17:10:44 -0800896 self.reviews.append({
897 'state': state,
898 'user': {
899 'login': user,
900 'email': user + "@derp.com",
901 },
Adam Gandelmand81dd762017-02-09 15:15:49 -0800902 'submitted_at': submitted_at,
Jesse Keatingae4cd272017-01-30 17:10:44 -0800903 })
904
Gregory Haynes4fc12542015-04-22 20:38:06 -0700905 def _getPRReference(self):
906 return '%s/head' % self.number
907
908 def _getPullRequestEvent(self, action):
909 name = 'pull_request'
910 data = {
911 'action': action,
912 'number': self.number,
913 'pull_request': {
914 'number': self.number,
Jan Hruban3b415922016-02-03 13:10:22 +0100915 'title': self.subject,
Gregory Haynes4fc12542015-04-22 20:38:06 -0700916 'updated_at': self.updated_at,
917 'base': {
918 'ref': self.branch,
919 'repo': {
920 'full_name': self.project
921 }
922 },
923 'head': {
Jesse Keatingd96e5882017-01-19 13:55:50 -0800924 'sha': self.head_sha,
925 'repo': {
926 'full_name': self.project
927 }
Jesse Keatinga41566f2017-06-14 18:17:51 -0700928 },
929 'body': self.body
Jan Hruban3b415922016-02-03 13:10:22 +0100930 },
931 'sender': {
932 'login': 'ghuser'
Gregory Haynes4fc12542015-04-22 20:38:06 -0700933 }
934 }
935 return (name, data)
936
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800937 def getCommitStatusEvent(self, context, state='success', user='zuul'):
938 name = 'status'
939 data = {
940 'state': state,
941 'sha': self.head_sha,
Jesse Keating9021a012017-08-29 14:45:27 -0700942 'name': self.project,
Adam Gandelman8c6eeb52017-01-23 16:31:06 -0800943 'description': 'Test results for %s: %s' % (self.head_sha, state),
944 'target_url': 'http://zuul/%s' % self.head_sha,
945 'branches': [],
946 'context': context,
947 'sender': {
948 'login': user
949 }
950 }
951 return (name, data)
952
James E. Blair289f5932017-07-27 15:02:29 -0700953 def setMerged(self, commit_message):
954 self.is_merged = True
955 self.merge_message = commit_message
956
957 repo = self._getRepo()
958 repo.heads[self.branch].commit = repo.commit(self.head_sha)
959
Gregory Haynes4fc12542015-04-22 20:38:06 -0700960
961class FakeGithubConnection(githubconnection.GithubConnection):
962 log = logging.getLogger("zuul.test.FakeGithubConnection")
963
Jesse Keating80730e62017-09-14 15:35:11 -0600964 def __init__(self, driver, connection_name, connection_config, rpcclient,
Tobias Henkel054eccc2017-12-20 11:36:19 +0100965 changes_db=None, upstream_root=None, git_url_with_auth=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700966 super(FakeGithubConnection, self).__init__(driver, connection_name,
967 connection_config)
968 self.connection_name = connection_name
969 self.pr_number = 0
James E. Blair6bacffb2018-01-05 13:45:25 -0800970 self.pull_requests = changes_db
Jesse Keating1f7ebe92017-06-12 17:21:00 -0700971 self.statuses = {}
Gregory Haynes4fc12542015-04-22 20:38:06 -0700972 self.upstream_root = upstream_root
Jan Hruban49bff072015-11-03 11:45:46 +0100973 self.merge_failure = False
974 self.merge_not_allowed_count = 0
Jesse Keating08dab8f2017-06-21 12:59:23 +0100975 self.reports = []
James E. Blair6bacffb2018-01-05 13:45:25 -0800976 self.github_client = tests.fakegithub.FakeGithub(changes_db)
Tobias Henkel054eccc2017-12-20 11:36:19 +0100977 self.git_url_with_auth = git_url_with_auth
Jesse Keating80730e62017-09-14 15:35:11 -0600978 self.rpcclient = rpcclient
Tobias Henkel64e37a02017-08-02 10:13:30 +0200979
980 def getGithubClient(self,
981 project=None,
Jesse Keating97b42482017-09-12 16:13:13 -0600982 user_id=None):
Tobias Henkel64e37a02017-08-02 10:13:30 +0200983 return self.github_client
Gregory Haynes4fc12542015-04-22 20:38:06 -0700984
Jesse Keating80730e62017-09-14 15:35:11 -0600985 def setZuulWebPort(self, port):
986 self.zuul_web_port = port
987
Jesse Keatinga41566f2017-06-14 18:17:51 -0700988 def openFakePullRequest(self, project, branch, subject, files=[],
Jesse Keating152a4022017-07-07 08:39:52 -0700989 body=None):
Gregory Haynes4fc12542015-04-22 20:38:06 -0700990 self.pr_number += 1
991 pull_request = FakeGithubPullRequest(
Jan Hruban570d01c2016-03-10 21:51:32 +0100992 self, self.pr_number, project, branch, subject, self.upstream_root,
Jesse Keatinga41566f2017-06-14 18:17:51 -0700993 files=files, body=body)
James E. Blair6bacffb2018-01-05 13:45:25 -0800994 self.pull_requests[self.pr_number] = pull_request
Gregory Haynes4fc12542015-04-22 20:38:06 -0700995 return pull_request
996
Jesse Keating71a47ff2017-06-06 11:36:43 -0700997 def getPushEvent(self, project, ref, old_rev=None, new_rev=None,
998 added_files=[], removed_files=[], modified_files=[]):
Wayne1a78c612015-06-11 17:14:13 -0700999 if not old_rev:
James E. Blairb8203e42017-08-02 17:00:14 -07001000 old_rev = '0' * 40
Wayne1a78c612015-06-11 17:14:13 -07001001 if not new_rev:
1002 new_rev = random_sha1()
1003 name = 'push'
1004 data = {
1005 'ref': ref,
1006 'before': old_rev,
1007 'after': new_rev,
1008 'repository': {
1009 'full_name': project
Jesse Keating71a47ff2017-06-06 11:36:43 -07001010 },
1011 'commits': [
1012 {
1013 'added': added_files,
1014 'removed': removed_files,
1015 'modified': modified_files
1016 }
1017 ]
Wayne1a78c612015-06-11 17:14:13 -07001018 }
1019 return (name, data)
1020
Jesse Keating80730e62017-09-14 15:35:11 -06001021 def emitEvent(self, event, use_zuulweb=False):
Gregory Haynes4fc12542015-04-22 20:38:06 -07001022 """Emulates sending the GitHub webhook event to the connection."""
Gregory Haynes4fc12542015-04-22 20:38:06 -07001023 name, data = event
Clint Byrum607d10e2017-05-18 12:05:13 -07001024 payload = json.dumps(data).encode('utf8')
Clint Byrumcf1b7422017-07-27 17:12:00 -07001025 secret = self.connection_config['webhook_token']
1026 signature = githubconnection._sign_request(payload, secret)
Jesse Keating80730e62017-09-14 15:35:11 -06001027 headers = {'x-github-event': name, 'x-hub-signature': signature}
1028
1029 if use_zuulweb:
1030 req = urllib.request.Request(
Monty Taylor64bf8e02018-01-23 16:39:30 -06001031 'http://127.0.0.1:%s/connection/%s/payload'
Jesse Keating80730e62017-09-14 15:35:11 -06001032 % (self.zuul_web_port, self.connection_name),
1033 data=payload, headers=headers)
1034 return urllib.request.urlopen(req)
1035 else:
1036 job = self.rpcclient.submitJob(
1037 'github:%s:payload' % self.connection_name,
1038 {'headers': headers, 'body': data})
1039 return json.loads(job.data[0])
Gregory Haynes4fc12542015-04-22 20:38:06 -07001040
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001041 def addProject(self, project):
1042 # use the original method here and additionally register it in the
1043 # fake github
1044 super(FakeGithubConnection, self).addProject(project)
1045 self.getGithubClient(project).addProject(project)
1046
Jesse Keating9021a012017-08-29 14:45:27 -07001047 def getPullBySha(self, sha, project):
James E. Blair6bacffb2018-01-05 13:45:25 -08001048 prs = list(set([p for p in self.pull_requests.values() if
Jesse Keating9021a012017-08-29 14:45:27 -07001049 sha == p.head_sha and project == p.project]))
Adam Gandelman8c6eeb52017-01-23 16:31:06 -08001050 if len(prs) > 1:
1051 raise Exception('Multiple pulls found with head sha: %s' % sha)
1052 pr = prs[0]
1053 return self.getPull(pr.project, pr.number)
1054
Jesse Keatingae4cd272017-01-30 17:10:44 -08001055 def _getPullReviews(self, owner, project, number):
James E. Blair6bacffb2018-01-05 13:45:25 -08001056 pr = self.pull_requests[number]
Jesse Keatingae4cd272017-01-30 17:10:44 -08001057 return pr.reviews
1058
Jesse Keatingae4cd272017-01-30 17:10:44 -08001059 def getRepoPermission(self, project, login):
1060 owner, proj = project.split('/')
James E. Blair6bacffb2018-01-05 13:45:25 -08001061 for pr in self.pull_requests.values():
Jesse Keatingae4cd272017-01-30 17:10:44 -08001062 pr_owner, pr_project = pr.project.split('/')
1063 if (pr_owner == owner and proj == pr_project):
1064 if login in pr.writers:
1065 return 'write'
1066 else:
1067 return 'read'
1068
Gregory Haynes4fc12542015-04-22 20:38:06 -07001069 def getGitUrl(self, project):
Tobias Henkel054eccc2017-12-20 11:36:19 +01001070 if self.git_url_with_auth:
1071 auth_token = ''.join(
1072 random.choice(string.ascii_lowercase) for x in range(8))
1073 prefix = 'file://x-access-token:%s@' % auth_token
1074 else:
1075 prefix = ''
1076 return prefix + os.path.join(self.upstream_root, str(project))
Gregory Haynes4fc12542015-04-22 20:38:06 -07001077
Jan Hruban6d53c5e2015-10-24 03:03:34 +02001078 def real_getGitUrl(self, project):
1079 return super(FakeGithubConnection, self).getGitUrl(project)
1080
Jan Hrubane252a732017-01-03 15:03:09 +01001081 def commentPull(self, project, pr_number, message):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001082 # record that this got reported
1083 self.reports.append((project, pr_number, 'comment'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001084 pull_request = self.pull_requests[pr_number]
Wayne40f40042015-06-12 16:56:30 -07001085 pull_request.addComment(message)
1086
Jan Hruban3b415922016-02-03 13:10:22 +01001087 def mergePull(self, project, pr_number, commit_message='', sha=None):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001088 # record that this got reported
1089 self.reports.append((project, pr_number, 'merge'))
James E. Blair6bacffb2018-01-05 13:45:25 -08001090 pull_request = self.pull_requests[pr_number]
Jan Hruban49bff072015-11-03 11:45:46 +01001091 if self.merge_failure:
1092 raise Exception('Pull request was not merged')
1093 if self.merge_not_allowed_count > 0:
1094 self.merge_not_allowed_count -= 1
1095 raise MergeFailure('Merge was not successful due to mergeability'
1096 ' conflict')
James E. Blair289f5932017-07-27 15:02:29 -07001097 pull_request.setMerged(commit_message)
Jan Hruban49bff072015-11-03 11:45:46 +01001098
Jesse Keating1f7ebe92017-06-12 17:21:00 -07001099 def setCommitStatus(self, project, sha, state, url='', description='',
1100 context='default', user='zuul'):
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001101 # record that this got reported and call original method
Jesse Keating08dab8f2017-06-21 12:59:23 +01001102 self.reports.append((project, sha, 'status', (user, context, state)))
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02001103 super(FakeGithubConnection, self).setCommitStatus(
1104 project, sha, state,
1105 url=url, description=description, context=context)
Jan Hrubane252a732017-01-03 15:03:09 +01001106
Jan Hruban16ad31f2015-11-07 14:39:07 +01001107 def labelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001108 # record that this got reported
1109 self.reports.append((project, pr_number, 'label', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001110 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001111 pull_request.addLabel(label)
1112
1113 def unlabelPull(self, project, pr_number, label):
Jesse Keating08dab8f2017-06-21 12:59:23 +01001114 # record that this got reported
1115 self.reports.append((project, pr_number, 'unlabel', label))
James E. Blair6bacffb2018-01-05 13:45:25 -08001116 pull_request = self.pull_requests[pr_number]
Jan Hruban16ad31f2015-11-07 14:39:07 +01001117 pull_request.removeLabel(label)
1118
Gregory Haynes4fc12542015-04-22 20:38:06 -07001119
Clark Boylanb640e052014-04-03 16:41:46 -07001120class BuildHistory(object):
1121 def __init__(self, **kw):
1122 self.__dict__.update(kw)
1123
1124 def __repr__(self):
James E. Blair21037782017-07-19 11:56:55 -07001125 return ("<Completed build, result: %s name: %s uuid: %s "
1126 "changes: %s ref: %s>" %
1127 (self.result, self.name, self.uuid,
1128 self.changes, self.ref))
Clark Boylanb640e052014-04-03 16:41:46 -07001129
1130
Clark Boylanb640e052014-04-03 16:41:46 -07001131class FakeStatsd(threading.Thread):
1132 def __init__(self):
1133 threading.Thread.__init__(self)
1134 self.daemon = True
Monty Taylor211883d2017-09-06 08:40:47 -05001135 self.sock = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
Clark Boylanb640e052014-04-03 16:41:46 -07001136 self.sock.bind(('', 0))
1137 self.port = self.sock.getsockname()[1]
1138 self.wake_read, self.wake_write = os.pipe()
1139 self.stats = []
1140
1141 def run(self):
1142 while True:
1143 poll = select.poll()
1144 poll.register(self.sock, select.POLLIN)
1145 poll.register(self.wake_read, select.POLLIN)
1146 ret = poll.poll()
1147 for (fd, event) in ret:
1148 if fd == self.sock.fileno():
1149 data = self.sock.recvfrom(1024)
1150 if not data:
1151 return
1152 self.stats.append(data[0])
1153 if fd == self.wake_read:
1154 return
1155
1156 def stop(self):
Clint Byrumf322fe22017-05-10 20:53:12 -07001157 os.write(self.wake_write, b'1\n')
Clark Boylanb640e052014-04-03 16:41:46 -07001158
1159
James E. Blaire1767bc2016-08-02 10:00:27 -07001160class FakeBuild(object):
Clark Boylanb640e052014-04-03 16:41:46 -07001161 log = logging.getLogger("zuul.test")
1162
Paul Belanger174a8272017-03-14 13:20:10 -04001163 def __init__(self, executor_server, job):
Clark Boylanb640e052014-04-03 16:41:46 -07001164 self.daemon = True
Paul Belanger174a8272017-03-14 13:20:10 -04001165 self.executor_server = executor_server
Clark Boylanb640e052014-04-03 16:41:46 -07001166 self.job = job
James E. Blairab7132b2016-08-05 12:36:22 -07001167 self.jobdir = None
James E. Blair17302972016-08-10 16:11:42 -07001168 self.uuid = job.unique
Clark Boylanb640e052014-04-03 16:41:46 -07001169 self.parameters = json.loads(job.arguments)
James E. Blair16d96a02017-06-08 11:32:56 -07001170 # TODOv3(jeblair): self.node is really "the label of the node
1171 # assigned". We should rename it (self.node_label?) if we
James E. Blair34776ee2016-08-25 13:53:54 -07001172 # keep using it like this, or we may end up exposing more of
1173 # the complexity around multi-node jobs here
James E. Blair16d96a02017-06-08 11:32:56 -07001174 # (self.nodes[0].label?)
James E. Blair34776ee2016-08-25 13:53:54 -07001175 self.node = None
1176 if len(self.parameters.get('nodes')) == 1:
James E. Blair16d96a02017-06-08 11:32:56 -07001177 self.node = self.parameters['nodes'][0]['label']
James E. Blair74f101b2017-07-21 15:32:01 -07001178 self.unique = self.parameters['zuul']['build']
James E. Blaire675d682017-07-21 15:29:35 -07001179 self.pipeline = self.parameters['zuul']['pipeline']
James E. Blaire5366092017-07-21 15:30:39 -07001180 self.project = self.parameters['zuul']['project']['name']
James E. Blair3f876d52016-07-22 13:07:14 -07001181 self.name = self.parameters['job']
Clark Boylanb640e052014-04-03 16:41:46 -07001182 self.wait_condition = threading.Condition()
1183 self.waiting = False
1184 self.aborted = False
Paul Belanger71d98172016-11-08 10:56:31 -05001185 self.requeue = False
Clark Boylanb640e052014-04-03 16:41:46 -07001186 self.created = time.time()
James E. Blaire1767bc2016-08-02 10:00:27 -07001187 self.changes = None
James E. Blair6193a1f2017-07-21 15:13:15 -07001188 items = self.parameters['zuul']['items']
1189 self.changes = ' '.join(['%s,%s' % (x['change'], x['patchset'])
1190 for x in items if 'change' in x])
Clark Boylanb640e052014-04-03 16:41:46 -07001191
James E. Blair3158e282016-08-19 09:34:11 -07001192 def __repr__(self):
1193 waiting = ''
1194 if self.waiting:
1195 waiting = ' [waiting]'
Jamie Lennoxd2e37332016-12-05 15:26:19 +11001196 return '<FakeBuild %s:%s %s%s>' % (self.pipeline, self.name,
1197 self.changes, waiting)
James E. Blair3158e282016-08-19 09:34:11 -07001198
Clark Boylanb640e052014-04-03 16:41:46 -07001199 def release(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001200 """Release this build."""
Clark Boylanb640e052014-04-03 16:41:46 -07001201 self.wait_condition.acquire()
1202 self.wait_condition.notify()
1203 self.waiting = False
1204 self.log.debug("Build %s released" % self.unique)
1205 self.wait_condition.release()
1206
1207 def isWaiting(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07001208 """Return whether this build is being held.
1209
1210 :returns: Whether the build is being held.
1211 :rtype: bool
1212 """
1213
Clark Boylanb640e052014-04-03 16:41:46 -07001214 self.wait_condition.acquire()
1215 if self.waiting:
1216 ret = True
1217 else:
1218 ret = False
1219 self.wait_condition.release()
1220 return ret
1221
1222 def _wait(self):
1223 self.wait_condition.acquire()
1224 self.waiting = True
1225 self.log.debug("Build %s waiting" % self.unique)
1226 self.wait_condition.wait()
1227 self.wait_condition.release()
1228
1229 def run(self):
Clark Boylanb640e052014-04-03 16:41:46 -07001230 self.log.debug('Running build %s' % self.unique)
1231
Paul Belanger174a8272017-03-14 13:20:10 -04001232 if self.executor_server.hold_jobs_in_build:
Clark Boylanb640e052014-04-03 16:41:46 -07001233 self.log.debug('Holding build %s' % self.unique)
1234 self._wait()
1235 self.log.debug("Build %s continuing" % self.unique)
1236
James E. Blair412fba82017-01-26 15:00:50 -08001237 result = (RecordingAnsibleJob.RESULT_NORMAL, 0) # Success
James E. Blair247cab72017-07-20 16:52:36 -07001238 if self.shouldFail():
James E. Blair412fba82017-01-26 15:00:50 -08001239 result = (RecordingAnsibleJob.RESULT_NORMAL, 1) # Failure
Clark Boylanb640e052014-04-03 16:41:46 -07001240 if self.aborted:
James E. Blair412fba82017-01-26 15:00:50 -08001241 result = (RecordingAnsibleJob.RESULT_ABORTED, None)
Paul Belanger71d98172016-11-08 10:56:31 -05001242 if self.requeue:
James E. Blair412fba82017-01-26 15:00:50 -08001243 result = (RecordingAnsibleJob.RESULT_UNREACHABLE, None)
Clark Boylanb640e052014-04-03 16:41:46 -07001244
James E. Blaire1767bc2016-08-02 10:00:27 -07001245 return result
Clark Boylanb640e052014-04-03 16:41:46 -07001246
James E. Blaira5dba232016-08-08 15:53:24 -07001247 def shouldFail(self):
Paul Belanger174a8272017-03-14 13:20:10 -04001248 changes = self.executor_server.fail_tests.get(self.name, [])
James E. Blaira5dba232016-08-08 15:53:24 -07001249 for change in changes:
1250 if self.hasChanges(change):
1251 return True
1252 return False
1253
James E. Blaire7b99a02016-08-05 14:27:34 -07001254 def hasChanges(self, *changes):
1255 """Return whether this build has certain changes in its git repos.
1256
1257 :arg FakeChange changes: One or more changes (varargs) that
Clark Boylan500992b2017-04-03 14:28:24 -07001258 are expected to be present (in order) in the git repository of
1259 the active project.
James E. Blaire7b99a02016-08-05 14:27:34 -07001260
1261 :returns: Whether the build has the indicated changes.
1262 :rtype: bool
1263
1264 """
Clint Byrum3343e3e2016-11-15 16:05:03 -08001265 for change in changes:
Gregory Haynes4fc12542015-04-22 20:38:06 -07001266 hostname = change.source.canonical_hostname
James E. Blair2a535672017-04-27 12:03:15 -07001267 path = os.path.join(self.jobdir.src_root, hostname, change.project)
Clint Byrum3343e3e2016-11-15 16:05:03 -08001268 try:
1269 repo = git.Repo(path)
1270 except NoSuchPathError as e:
1271 self.log.debug('%s' % e)
1272 return False
James E. Blair247cab72017-07-20 16:52:36 -07001273 repo_messages = [c.message.strip() for c in repo.iter_commits()]
Clint Byrum3343e3e2016-11-15 16:05:03 -08001274 commit_message = '%s-1' % change.subject
1275 self.log.debug("Checking if build %s has changes; commit_message "
1276 "%s; repo_messages %s" % (self, commit_message,
1277 repo_messages))
1278 if commit_message not in repo_messages:
James E. Blair962220f2016-08-03 11:22:38 -07001279 self.log.debug(" messages do not match")
1280 return False
1281 self.log.debug(" OK")
1282 return True
1283
James E. Blaird8af5422017-05-24 13:59:40 -07001284 def getWorkspaceRepos(self, projects):
1285 """Return workspace git repo objects for the listed projects
1286
1287 :arg list projects: A list of strings, each the canonical name
1288 of a project.
1289
1290 :returns: A dictionary of {name: repo} for every listed
1291 project.
1292 :rtype: dict
1293
1294 """
1295
1296 repos = {}
1297 for project in projects:
1298 path = os.path.join(self.jobdir.src_root, project)
1299 repo = git.Repo(path)
1300 repos[project] = repo
1301 return repos
1302
Clark Boylanb640e052014-04-03 16:41:46 -07001303
James E. Blair107bb252017-10-13 15:53:16 -07001304class RecordingAnsibleJob(zuul.executor.server.AnsibleJob):
1305 def doMergeChanges(self, merger, items, repo_state):
1306 # Get a merger in order to update the repos involved in this job.
1307 commit = super(RecordingAnsibleJob, self).doMergeChanges(
1308 merger, items, repo_state)
1309 if not commit: # merge conflict
1310 self.recordResult('MERGER_FAILURE')
1311 return commit
1312
1313 def recordResult(self, result):
1314 build = self.executor_server.job_builds[self.job.unique]
1315 self.executor_server.lock.acquire()
1316 self.executor_server.build_history.append(
1317 BuildHistory(name=build.name, result=result, changes=build.changes,
1318 node=build.node, uuid=build.unique,
1319 ref=build.parameters['zuul']['ref'],
1320 parameters=build.parameters, jobdir=build.jobdir,
1321 pipeline=build.parameters['zuul']['pipeline'])
1322 )
1323 self.executor_server.running_builds.remove(build)
1324 del self.executor_server.job_builds[self.job.unique]
1325 self.executor_server.lock.release()
1326
1327 def runPlaybooks(self, args):
1328 build = self.executor_server.job_builds[self.job.unique]
1329 build.jobdir = self.jobdir
1330
1331 result = super(RecordingAnsibleJob, self).runPlaybooks(args)
1332 self.recordResult(result)
1333 return result
1334
James E. Blaira86aaf12017-10-15 20:59:50 -07001335 def runAnsible(self, cmd, timeout, playbook, wrapped=True):
James E. Blair107bb252017-10-13 15:53:16 -07001336 build = self.executor_server.job_builds[self.job.unique]
1337
1338 if self.executor_server._run_ansible:
1339 result = super(RecordingAnsibleJob, self).runAnsible(
James E. Blaira86aaf12017-10-15 20:59:50 -07001340 cmd, timeout, playbook, wrapped)
James E. Blair107bb252017-10-13 15:53:16 -07001341 else:
1342 if playbook.path:
1343 result = build.run()
1344 else:
1345 result = (self.RESULT_NORMAL, 0)
1346 return result
1347
1348 def getHostList(self, args):
1349 self.log.debug("hostlist")
1350 hosts = super(RecordingAnsibleJob, self).getHostList(args)
1351 for host in hosts:
Tobias Henkelc5043212017-09-08 08:53:47 +02001352 if not host['host_vars'].get('ansible_connection'):
1353 host['host_vars']['ansible_connection'] = 'local'
James E. Blair107bb252017-10-13 15:53:16 -07001354
1355 hosts.append(dict(
James E. Blair67cd8592018-02-14 09:30:07 -08001356 name='localhost',
James E. Blair107bb252017-10-13 15:53:16 -07001357 host_vars=dict(ansible_connection='local'),
1358 host_keys=[]))
1359 return hosts
1360
1361
Paul Belanger174a8272017-03-14 13:20:10 -04001362class RecordingExecutorServer(zuul.executor.server.ExecutorServer):
1363 """An Ansible executor to be used in tests.
James E. Blaire7b99a02016-08-05 14:27:34 -07001364
Paul Belanger174a8272017-03-14 13:20:10 -04001365 :ivar bool hold_jobs_in_build: If true, when jobs are executed
James E. Blaire7b99a02016-08-05 14:27:34 -07001366 they will report that they have started but then pause until
1367 released before reporting completion. This attribute may be
1368 changed at any time and will take effect for subsequently
Paul Belanger174a8272017-03-14 13:20:10 -04001369 executed builds, but previously held builds will still need to
James E. Blaire7b99a02016-08-05 14:27:34 -07001370 be explicitly released.
1371
1372 """
James E. Blairfaf81982017-10-10 15:42:26 -07001373
1374 _job_class = RecordingAnsibleJob
1375
James E. Blairf5dbd002015-12-23 15:26:17 -08001376 def __init__(self, *args, **kw):
James E. Blaire1767bc2016-08-02 10:00:27 -07001377 self._run_ansible = kw.pop('_run_ansible', False)
James E. Blaira92cbc82017-01-23 14:56:49 -08001378 self._test_root = kw.pop('_test_root', False)
Paul Belanger174a8272017-03-14 13:20:10 -04001379 super(RecordingExecutorServer, self).__init__(*args, **kw)
James E. Blaire1767bc2016-08-02 10:00:27 -07001380 self.hold_jobs_in_build = False
1381 self.lock = threading.Lock()
1382 self.running_builds = []
James E. Blair3f876d52016-07-22 13:07:14 -07001383 self.build_history = []
James E. Blaire1767bc2016-08-02 10:00:27 -07001384 self.fail_tests = {}
James E. Blairab7132b2016-08-05 12:36:22 -07001385 self.job_builds = {}
James E. Blairf5dbd002015-12-23 15:26:17 -08001386
James E. Blaira5dba232016-08-08 15:53:24 -07001387 def failJob(self, name, change):
Paul Belanger174a8272017-03-14 13:20:10 -04001388 """Instruct the executor to report matching builds as failures.
James E. Blaire7b99a02016-08-05 14:27:34 -07001389
1390 :arg str name: The name of the job to fail.
James E. Blaira5dba232016-08-08 15:53:24 -07001391 :arg Change change: The :py:class:`~tests.base.FakeChange`
1392 instance which should cause the job to fail. This job
1393 will also fail for changes depending on this change.
James E. Blaire7b99a02016-08-05 14:27:34 -07001394
1395 """
James E. Blaire1767bc2016-08-02 10:00:27 -07001396 l = self.fail_tests.get(name, [])
1397 l.append(change)
1398 self.fail_tests[name] = l
James E. Blairf5dbd002015-12-23 15:26:17 -08001399
James E. Blair962220f2016-08-03 11:22:38 -07001400 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001401 """Release a held build.
1402
1403 :arg str regex: A regular expression which, if supplied, will
1404 cause only builds with matching names to be released. If
1405 not supplied, all builds will be released.
1406
1407 """
James E. Blair962220f2016-08-03 11:22:38 -07001408 builds = self.running_builds[:]
1409 self.log.debug("Releasing build %s (%s)" % (regex,
1410 len(self.running_builds)))
1411 for build in builds:
1412 if not regex or re.match(regex, build.name):
1413 self.log.debug("Releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001414 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001415 build.release()
1416 else:
1417 self.log.debug("Not releasing build %s" %
James E. Blair74f101b2017-07-21 15:32:01 -07001418 (build.parameters['zuul']['build']))
James E. Blair962220f2016-08-03 11:22:38 -07001419 self.log.debug("Done releasing builds %s (%s)" %
1420 (regex, len(self.running_builds)))
1421
Paul Belanger174a8272017-03-14 13:20:10 -04001422 def executeJob(self, job):
James E. Blair34776ee2016-08-25 13:53:54 -07001423 build = FakeBuild(self, job)
James E. Blaire1767bc2016-08-02 10:00:27 -07001424 job.build = build
James E. Blaire1767bc2016-08-02 10:00:27 -07001425 self.running_builds.append(build)
James E. Blairab7132b2016-08-05 12:36:22 -07001426 self.job_builds[job.unique] = build
James E. Blaira92cbc82017-01-23 14:56:49 -08001427 args = json.loads(job.arguments)
Monty Taylord13bc362017-06-30 13:11:37 -05001428 args['zuul']['_test'] = dict(test_root=self._test_root)
James E. Blaira92cbc82017-01-23 14:56:49 -08001429 job.arguments = json.dumps(args)
James E. Blairfaf81982017-10-10 15:42:26 -07001430 super(RecordingExecutorServer, self).executeJob(job)
James E. Blair17302972016-08-10 16:11:42 -07001431
1432 def stopJob(self, job):
1433 self.log.debug("handle stop")
1434 parameters = json.loads(job.arguments)
1435 uuid = parameters['uuid']
1436 for build in self.running_builds:
1437 if build.unique == uuid:
1438 build.aborted = True
1439 build.release()
Paul Belanger174a8272017-03-14 13:20:10 -04001440 super(RecordingExecutorServer, self).stopJob(job)
James E. Blairab7132b2016-08-05 12:36:22 -07001441
James E. Blaira002b032017-04-18 10:35:48 -07001442 def stop(self):
1443 for build in self.running_builds:
1444 build.release()
1445 super(RecordingExecutorServer, self).stop()
1446
Joshua Hesketh50c21782016-10-13 21:34:14 +11001447
Clark Boylanb640e052014-04-03 16:41:46 -07001448class FakeGearmanServer(gear.Server):
James E. Blaire7b99a02016-08-05 14:27:34 -07001449 """A Gearman server for use in tests.
1450
1451 :ivar bool hold_jobs_in_queue: If true, submitted jobs will be
1452 added to the queue but will not be distributed to workers
1453 until released. This attribute may be changed at any time and
1454 will take effect for subsequently enqueued jobs, but
1455 previously held jobs will still need to be explicitly
1456 released.
1457
1458 """
1459
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001460 def __init__(self, use_ssl=False):
Clark Boylanb640e052014-04-03 16:41:46 -07001461 self.hold_jobs_in_queue = False
James E. Blaira615c362017-10-02 17:34:42 -07001462 self.hold_merge_jobs_in_queue = False
Fabien Boucher52252312018-01-18 19:54:34 +01001463 self.jobs_history = []
Paul Belanger0a21f0a2017-06-13 13:14:42 -04001464 if use_ssl:
1465 ssl_ca = os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem')
1466 ssl_cert = os.path.join(FIXTURE_DIR, 'gearman/server.pem')
1467 ssl_key = os.path.join(FIXTURE_DIR, 'gearman/server.key')
1468 else:
1469 ssl_ca = None
1470 ssl_cert = None
1471 ssl_key = None
1472
1473 super(FakeGearmanServer, self).__init__(0, ssl_key=ssl_key,
1474 ssl_cert=ssl_cert,
1475 ssl_ca=ssl_ca)
Clark Boylanb640e052014-04-03 16:41:46 -07001476
1477 def getJobForConnection(self, connection, peek=False):
Monty Taylorb934c1a2017-06-16 19:31:47 -05001478 for job_queue in [self.high_queue, self.normal_queue, self.low_queue]:
1479 for job in job_queue:
Fabien Boucher52252312018-01-18 19:54:34 +01001480 self.jobs_history.append(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001481 if not hasattr(job, 'waiting'):
Clint Byrumf322fe22017-05-10 20:53:12 -07001482 if job.name.startswith(b'executor:execute'):
Clark Boylanb640e052014-04-03 16:41:46 -07001483 job.waiting = self.hold_jobs_in_queue
James E. Blaira615c362017-10-02 17:34:42 -07001484 elif job.name.startswith(b'merger:'):
1485 job.waiting = self.hold_merge_jobs_in_queue
Clark Boylanb640e052014-04-03 16:41:46 -07001486 else:
1487 job.waiting = False
1488 if job.waiting:
1489 continue
1490 if job.name in connection.functions:
1491 if not peek:
Monty Taylorb934c1a2017-06-16 19:31:47 -05001492 job_queue.remove(job)
Clark Boylanb640e052014-04-03 16:41:46 -07001493 connection.related_jobs[job.handle] = job
1494 job.worker_connection = connection
1495 job.running = True
1496 return job
1497 return None
1498
1499 def release(self, regex=None):
James E. Blaire7b99a02016-08-05 14:27:34 -07001500 """Release a held job.
1501
1502 :arg str regex: A regular expression which, if supplied, will
1503 cause only jobs with matching names to be released. If
1504 not supplied, all jobs will be released.
1505 """
Clark Boylanb640e052014-04-03 16:41:46 -07001506 released = False
1507 qlen = (len(self.high_queue) + len(self.normal_queue) +
1508 len(self.low_queue))
1509 self.log.debug("releasing queued job %s (%s)" % (regex, qlen))
1510 for job in self.getQueue():
James E. Blaira615c362017-10-02 17:34:42 -07001511 match = False
1512 if job.name == b'executor:execute':
1513 parameters = json.loads(job.arguments.decode('utf8'))
1514 if not regex or re.match(regex, parameters.get('job')):
1515 match = True
James E. Blair29c77002017-10-05 14:56:35 -07001516 if job.name.startswith(b'merger:'):
James E. Blaira615c362017-10-02 17:34:42 -07001517 if not regex:
1518 match = True
1519 if match:
Clark Boylanb640e052014-04-03 16:41:46 -07001520 self.log.debug("releasing queued job %s" %
1521 job.unique)
1522 job.waiting = False
1523 released = True
1524 else:
1525 self.log.debug("not releasing queued job %s" %
1526 job.unique)
1527 if released:
1528 self.wakeConnections()
1529 qlen = (len(self.high_queue) + len(self.normal_queue) +
1530 len(self.low_queue))
1531 self.log.debug("done releasing queued jobs %s (%s)" % (regex, qlen))
1532
1533
1534class FakeSMTP(object):
1535 log = logging.getLogger('zuul.FakeSMTP')
1536
1537 def __init__(self, messages, server, port):
1538 self.server = server
1539 self.port = port
1540 self.messages = messages
1541
1542 def sendmail(self, from_email, to_email, msg):
1543 self.log.info("Sending email from %s, to %s, with msg %s" % (
1544 from_email, to_email, msg))
1545
1546 headers = msg.split('\n\n', 1)[0]
1547 body = msg.split('\n\n', 1)[1]
1548
1549 self.messages.append(dict(
1550 from_email=from_email,
1551 to_email=to_email,
1552 msg=msg,
1553 headers=headers,
1554 body=body,
1555 ))
1556
1557 return True
1558
1559 def quit(self):
1560 return True
1561
1562
James E. Blairdce6cea2016-12-20 16:45:32 -08001563class FakeNodepool(object):
1564 REQUEST_ROOT = '/nodepool/requests'
James E. Blaire18d4602017-01-05 11:17:28 -08001565 NODE_ROOT = '/nodepool/nodes'
James E. Blairdce6cea2016-12-20 16:45:32 -08001566
1567 log = logging.getLogger("zuul.test.FakeNodepool")
1568
1569 def __init__(self, host, port, chroot):
1570 self.client = kazoo.client.KazooClient(
1571 hosts='%s:%s%s' % (host, port, chroot))
1572 self.client.start()
1573 self._running = True
James E. Blair15be0e12017-01-03 13:45:20 -08001574 self.paused = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001575 self.thread = threading.Thread(target=self.run)
1576 self.thread.daemon = True
1577 self.thread.start()
James E. Blair6ab79e02017-01-06 10:10:17 -08001578 self.fail_requests = set()
James E. Blairdce6cea2016-12-20 16:45:32 -08001579
1580 def stop(self):
1581 self._running = False
1582 self.thread.join()
1583 self.client.stop()
1584 self.client.close()
1585
1586 def run(self):
1587 while self._running:
James E. Blaircbbce0d2017-05-19 07:28:29 -07001588 try:
1589 self._run()
1590 except Exception:
1591 self.log.exception("Error in fake nodepool:")
James E. Blairdce6cea2016-12-20 16:45:32 -08001592 time.sleep(0.1)
1593
1594 def _run(self):
James E. Blair15be0e12017-01-03 13:45:20 -08001595 if self.paused:
1596 return
James E. Blairdce6cea2016-12-20 16:45:32 -08001597 for req in self.getNodeRequests():
1598 self.fulfillRequest(req)
1599
1600 def getNodeRequests(self):
1601 try:
1602 reqids = self.client.get_children(self.REQUEST_ROOT)
1603 except kazoo.exceptions.NoNodeError:
1604 return []
1605 reqs = []
1606 for oid in sorted(reqids):
1607 path = self.REQUEST_ROOT + '/' + oid
James E. Blair0ef64f82017-02-02 11:25:16 -08001608 try:
1609 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001610 data = json.loads(data.decode('utf8'))
James E. Blair0ef64f82017-02-02 11:25:16 -08001611 data['_oid'] = oid
1612 reqs.append(data)
1613 except kazoo.exceptions.NoNodeError:
1614 pass
James E. Blairdce6cea2016-12-20 16:45:32 -08001615 return reqs
1616
James E. Blaire18d4602017-01-05 11:17:28 -08001617 def getNodes(self):
1618 try:
1619 nodeids = self.client.get_children(self.NODE_ROOT)
1620 except kazoo.exceptions.NoNodeError:
1621 return []
1622 nodes = []
1623 for oid in sorted(nodeids):
1624 path = self.NODE_ROOT + '/' + oid
1625 data, stat = self.client.get(path)
Clint Byrumf322fe22017-05-10 20:53:12 -07001626 data = json.loads(data.decode('utf8'))
James E. Blaire18d4602017-01-05 11:17:28 -08001627 data['_oid'] = oid
1628 try:
1629 lockfiles = self.client.get_children(path + '/lock')
1630 except kazoo.exceptions.NoNodeError:
1631 lockfiles = []
1632 if lockfiles:
1633 data['_lock'] = True
1634 else:
1635 data['_lock'] = False
1636 nodes.append(data)
1637 return nodes
1638
James E. Blaira38c28e2017-01-04 10:33:20 -08001639 def makeNode(self, request_id, node_type):
1640 now = time.time()
1641 path = '/nodepool/nodes/'
1642 data = dict(type=node_type,
Paul Belangerd28c7552017-08-11 13:10:38 -04001643 cloud='test-cloud',
James E. Blaira38c28e2017-01-04 10:33:20 -08001644 provider='test-provider',
1645 region='test-region',
Paul Belanger30ba93a2017-03-16 16:28:10 -04001646 az='test-az',
Monty Taylor56f61332017-04-11 05:38:12 -05001647 interface_ip='127.0.0.1',
James E. Blaira38c28e2017-01-04 10:33:20 -08001648 public_ipv4='127.0.0.1',
1649 private_ipv4=None,
1650 public_ipv6=None,
1651 allocated_to=request_id,
1652 state='ready',
1653 state_time=now,
1654 created_time=now,
1655 updated_time=now,
1656 image_id=None,
Paul Belangerc5bf3752017-03-16 19:38:43 -04001657 host_keys=["fake-key1", "fake-key2"],
Paul Belanger174a8272017-03-14 13:20:10 -04001658 executor='fake-nodepool')
Jamie Lennoxd4006d62017-04-06 10:34:04 +10001659 if 'fakeuser' in node_type:
1660 data['username'] = 'fakeuser'
Tobias Henkelc5043212017-09-08 08:53:47 +02001661 if 'windows' in node_type:
1662 data['connection_type'] = 'winrm'
Ricardo Carrillo Cruz6eda4392017-12-27 19:34:47 +01001663 if 'network' in node_type:
1664 data['connection_type'] = 'network_cli'
Tobias Henkelc5043212017-09-08 08:53:47 +02001665
Clint Byrumf322fe22017-05-10 20:53:12 -07001666 data = json.dumps(data).encode('utf8')
James E. Blaira38c28e2017-01-04 10:33:20 -08001667 path = self.client.create(path, data,
1668 makepath=True,
1669 sequence=True)
1670 nodeid = path.split("/")[-1]
1671 return nodeid
1672
Krzysztof Klimonda37d54032017-10-25 12:16:47 +02001673 def removeNode(self, node):
1674 path = self.NODE_ROOT + '/' + node["_oid"]
1675 self.client.delete(path, recursive=True)
1676
James E. Blair6ab79e02017-01-06 10:10:17 -08001677 def addFailRequest(self, request):
1678 self.fail_requests.add(request['_oid'])
1679
James E. Blairdce6cea2016-12-20 16:45:32 -08001680 def fulfillRequest(self, request):
James E. Blair6ab79e02017-01-06 10:10:17 -08001681 if request['state'] != 'requested':
James E. Blairdce6cea2016-12-20 16:45:32 -08001682 return
1683 request = request.copy()
James E. Blairdce6cea2016-12-20 16:45:32 -08001684 oid = request['_oid']
1685 del request['_oid']
James E. Blaira38c28e2017-01-04 10:33:20 -08001686
James E. Blair6ab79e02017-01-06 10:10:17 -08001687 if oid in self.fail_requests:
1688 request['state'] = 'failed'
1689 else:
1690 request['state'] = 'fulfilled'
1691 nodes = []
1692 for node in request['node_types']:
1693 nodeid = self.makeNode(oid, node)
1694 nodes.append(nodeid)
1695 request['nodes'] = nodes
James E. Blaira38c28e2017-01-04 10:33:20 -08001696
James E. Blaira38c28e2017-01-04 10:33:20 -08001697 request['state_time'] = time.time()
James E. Blairdce6cea2016-12-20 16:45:32 -08001698 path = self.REQUEST_ROOT + '/' + oid
Clint Byrumf322fe22017-05-10 20:53:12 -07001699 data = json.dumps(request).encode('utf8')
James E. Blairdce6cea2016-12-20 16:45:32 -08001700 self.log.debug("Fulfilling node request: %s %s" % (oid, data))
James E. Blaircbbce0d2017-05-19 07:28:29 -07001701 try:
1702 self.client.set(path, data)
1703 except kazoo.exceptions.NoNodeError:
1704 self.log.debug("Node request %s %s disappeared" % (oid, data))
James E. Blairdce6cea2016-12-20 16:45:32 -08001705
1706
James E. Blair498059b2016-12-20 13:50:13 -08001707class ChrootedKazooFixture(fixtures.Fixture):
Clark Boylan621ec9a2017-04-07 17:41:33 -07001708 def __init__(self, test_id):
James E. Blair498059b2016-12-20 13:50:13 -08001709 super(ChrootedKazooFixture, self).__init__()
1710
1711 zk_host = os.environ.get('NODEPOOL_ZK_HOST', 'localhost')
1712 if ':' in zk_host:
1713 host, port = zk_host.split(':')
1714 else:
1715 host = zk_host
1716 port = None
1717
1718 self.zookeeper_host = host
1719
1720 if not port:
1721 self.zookeeper_port = 2181
1722 else:
1723 self.zookeeper_port = int(port)
1724
Clark Boylan621ec9a2017-04-07 17:41:33 -07001725 self.test_id = test_id
1726
James E. Blair498059b2016-12-20 13:50:13 -08001727 def _setUp(self):
1728 # Make sure the test chroot paths do not conflict
1729 random_bits = ''.join(random.choice(string.ascii_lowercase +
1730 string.ascii_uppercase)
1731 for x in range(8))
1732
Clark Boylan621ec9a2017-04-07 17:41:33 -07001733 rand_test_path = '%s_%s_%s' % (random_bits, os.getpid(), self.test_id)
James E. Blair498059b2016-12-20 13:50:13 -08001734 self.zookeeper_chroot = "/nodepool_test/%s" % rand_test_path
1735
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001736 self.addCleanup(self._cleanup)
1737
James E. Blair498059b2016-12-20 13:50:13 -08001738 # Ensure the chroot path exists and clean up any pre-existing znodes.
1739 _tmp_client = kazoo.client.KazooClient(
1740 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1741 _tmp_client.start()
1742
1743 if _tmp_client.exists(self.zookeeper_chroot):
1744 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1745
1746 _tmp_client.ensure_path(self.zookeeper_chroot)
1747 _tmp_client.stop()
1748 _tmp_client.close()
1749
James E. Blair498059b2016-12-20 13:50:13 -08001750 def _cleanup(self):
1751 '''Remove the chroot path.'''
1752 # Need a non-chroot'ed client to remove the chroot path
1753 _tmp_client = kazoo.client.KazooClient(
1754 hosts='%s:%s' % (self.zookeeper_host, self.zookeeper_port))
1755 _tmp_client.start()
1756 _tmp_client.delete(self.zookeeper_chroot, recursive=True)
1757 _tmp_client.stop()
Clark Boylan7f5f1ec2017-04-07 17:39:56 -07001758 _tmp_client.close()
James E. Blair498059b2016-12-20 13:50:13 -08001759
1760
Monty Taylor4a781a72017-07-25 07:28:04 -04001761class WebProxyFixture(fixtures.Fixture):
1762 def __init__(self, rules):
1763 super(WebProxyFixture, self).__init__()
1764 self.rules = rules
1765
1766 def _setUp(self):
1767 rules = self.rules
1768
1769 class Proxy(http.server.SimpleHTTPRequestHandler):
1770 def do_GET(self):
1771 path = self.path
1772 for (pattern, replace) in rules:
1773 path = re.sub(pattern, replace, path)
1774 try:
1775 remote = urllib.request.urlopen(path)
1776 except urllib.error.HTTPError as e:
1777 self.send_response(e.code)
1778 self.end_headers()
1779 return
1780 self.send_response(int(remote.getcode()))
1781 for header in remote.info():
1782 self.send_header(header, remote.info()[header])
1783 self.end_headers()
1784 self.wfile.write(remote.read())
1785
1786 self.httpd = socketserver.ThreadingTCPServer(('', 0), Proxy)
1787 self.port = self.httpd.socket.getsockname()[1]
1788 self.thread = threading.Thread(target=self.httpd.serve_forever)
1789 self.thread.start()
1790 self.addCleanup(self._cleanup)
1791
1792 def _cleanup(self):
1793 self.httpd.shutdown()
1794 self.thread.join()
1795
1796
1797class ZuulWebFixture(fixtures.Fixture):
1798 def __init__(self, gearman_server_port):
1799 super(ZuulWebFixture, self).__init__()
1800 self.gearman_server_port = gearman_server_port
1801
1802 def _setUp(self):
1803 # Start the web server
1804 self.web = zuul.web.ZuulWeb(
1805 listen_address='127.0.0.1', listen_port=0,
1806 gear_server='127.0.0.1', gear_port=self.gearman_server_port)
1807 loop = asyncio.new_event_loop()
1808 loop.set_debug(True)
1809 ws_thread = threading.Thread(target=self.web.run, args=(loop,))
1810 ws_thread.start()
1811 self.addCleanup(loop.close)
1812 self.addCleanup(ws_thread.join)
1813 self.addCleanup(self.web.stop)
1814
1815 self.host = 'localhost'
1816 # Wait until web server is started
1817 while True:
1818 time.sleep(0.1)
1819 if self.web.server is None:
1820 continue
1821 self.port = self.web.server.sockets[0].getsockname()[1]
1822 try:
1823 with socket.create_connection((self.host, self.port)):
1824 break
1825 except ConnectionRefusedError:
1826 pass
1827
1828
Joshua Heskethd78b4482015-09-14 16:56:34 -06001829class MySQLSchemaFixture(fixtures.Fixture):
1830 def setUp(self):
1831 super(MySQLSchemaFixture, self).setUp()
1832
1833 random_bits = ''.join(random.choice(string.ascii_lowercase +
1834 string.ascii_uppercase)
1835 for x in range(8))
1836 self.name = '%s_%s' % (random_bits, os.getpid())
1837 self.passwd = uuid.uuid4().hex
1838 db = pymysql.connect(host="localhost",
1839 user="openstack_citest",
1840 passwd="openstack_citest",
1841 db="openstack_citest")
1842 cur = db.cursor()
1843 cur.execute("create database %s" % self.name)
1844 cur.execute(
1845 "grant all on %s.* to '%s'@'localhost' identified by '%s'" %
1846 (self.name, self.name, self.passwd))
1847 cur.execute("flush privileges")
1848
1849 self.dburi = 'mysql+pymysql://%s:%s@localhost/%s' % (self.name,
1850 self.passwd,
1851 self.name)
1852 self.addDetail('dburi', testtools.content.text_content(self.dburi))
1853 self.addCleanup(self.cleanup)
1854
1855 def cleanup(self):
1856 db = pymysql.connect(host="localhost",
1857 user="openstack_citest",
1858 passwd="openstack_citest",
1859 db="openstack_citest")
1860 cur = db.cursor()
1861 cur.execute("drop database %s" % self.name)
1862 cur.execute("drop user '%s'@'localhost'" % self.name)
1863 cur.execute("flush privileges")
1864
1865
Maru Newby3fe5f852015-01-13 04:22:14 +00001866class BaseTestCase(testtools.TestCase):
Clark Boylanb640e052014-04-03 16:41:46 -07001867 log = logging.getLogger("zuul.test")
James E. Blair267e5162017-04-07 10:08:20 -07001868 wait_timeout = 30
Clark Boylanb640e052014-04-03 16:41:46 -07001869
James E. Blair1c236df2017-02-01 14:07:24 -08001870 def attachLogs(self, *args):
1871 def reader():
1872 self._log_stream.seek(0)
1873 while True:
1874 x = self._log_stream.read(4096)
1875 if not x:
1876 break
1877 yield x.encode('utf8')
1878 content = testtools.content.content_from_reader(
1879 reader,
1880 testtools.content_type.UTF8_TEXT,
1881 False)
1882 self.addDetail('logging', content)
1883
Clark Boylanb640e052014-04-03 16:41:46 -07001884 def setUp(self):
Maru Newby3fe5f852015-01-13 04:22:14 +00001885 super(BaseTestCase, self).setUp()
Clark Boylanb640e052014-04-03 16:41:46 -07001886 test_timeout = os.environ.get('OS_TEST_TIMEOUT', 0)
1887 try:
1888 test_timeout = int(test_timeout)
1889 except ValueError:
1890 # If timeout value is invalid do not set a timeout.
1891 test_timeout = 0
1892 if test_timeout > 0:
1893 self.useFixture(fixtures.Timeout(test_timeout, gentle=False))
1894
1895 if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
1896 os.environ.get('OS_STDOUT_CAPTURE') == '1'):
1897 stdout = self.useFixture(fixtures.StringStream('stdout')).stream
1898 self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
1899 if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
1900 os.environ.get('OS_STDERR_CAPTURE') == '1'):
1901 stderr = self.useFixture(fixtures.StringStream('stderr')).stream
1902 self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
1903 if (os.environ.get('OS_LOG_CAPTURE') == 'True' or
1904 os.environ.get('OS_LOG_CAPTURE') == '1'):
James E. Blair1c236df2017-02-01 14:07:24 -08001905 self._log_stream = StringIO()
1906 self.addOnException(self.attachLogs)
1907 else:
1908 self._log_stream = sys.stdout
Maru Newby3fe5f852015-01-13 04:22:14 +00001909
James E. Blair1c236df2017-02-01 14:07:24 -08001910 handler = logging.StreamHandler(self._log_stream)
1911 formatter = logging.Formatter('%(asctime)s %(name)-32s '
1912 '%(levelname)-8s %(message)s')
1913 handler.setFormatter(formatter)
1914
1915 logger = logging.getLogger()
1916 logger.setLevel(logging.DEBUG)
1917 logger.addHandler(handler)
1918
Clark Boylan3410d532017-04-25 12:35:29 -07001919 # Make sure we don't carry old handlers around in process state
1920 # which slows down test runs
1921 self.addCleanup(logger.removeHandler, handler)
1922 self.addCleanup(handler.close)
1923 self.addCleanup(handler.flush)
1924
James E. Blair1c236df2017-02-01 14:07:24 -08001925 # NOTE(notmorgan): Extract logging overrides for specific
1926 # libraries from the OS_LOG_DEFAULTS env and create loggers
1927 # for each. This is used to limit the output during test runs
1928 # from libraries that zuul depends on such as gear.
James E. Blairdce6cea2016-12-20 16:45:32 -08001929 log_defaults_from_env = os.environ.get(
1930 'OS_LOG_DEFAULTS',
Monty Taylor9961c032018-02-19 16:37:51 -06001931 'git.cmd=INFO,kazoo.client=WARNING,gear=INFO')
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001932
James E. Blairdce6cea2016-12-20 16:45:32 -08001933 if log_defaults_from_env:
1934 for default in log_defaults_from_env.split(','):
1935 try:
1936 name, level_str = default.split('=', 1)
1937 level = getattr(logging, level_str, logging.DEBUG)
James E. Blair1c236df2017-02-01 14:07:24 -08001938 logger = logging.getLogger(name)
1939 logger.setLevel(level)
1940 logger.addHandler(handler)
1941 logger.propagate = False
James E. Blairdce6cea2016-12-20 16:45:32 -08001942 except ValueError:
1943 # NOTE(notmorgan): Invalid format of the log default,
1944 # skip and don't try and apply a logger for the
1945 # specified module
1946 pass
Morgan Fainbergd34e0b42016-06-09 19:10:38 -07001947
Maru Newby3fe5f852015-01-13 04:22:14 +00001948
1949class ZuulTestCase(BaseTestCase):
James E. Blaire7b99a02016-08-05 14:27:34 -07001950 """A test case with a functioning Zuul.
1951
1952 The following class variables are used during test setup and can
1953 be overidden by subclasses but are effectively read-only once a
1954 test method starts running:
1955
1956 :cvar str config_file: This points to the main zuul config file
1957 within the fixtures directory. Subclasses may override this
1958 to obtain a different behavior.
1959
1960 :cvar str tenant_config_file: This is the tenant config file
1961 (which specifies from what git repos the configuration should
1962 be loaded). It defaults to the value specified in
1963 `config_file` but can be overidden by subclasses to obtain a
1964 different tenant/project layout while using the standard main
James E. Blair06cc3922017-04-19 10:08:10 -07001965 configuration. See also the :py:func:`simple_layout`
1966 decorator.
James E. Blaire7b99a02016-08-05 14:27:34 -07001967
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00001968 :cvar bool create_project_keys: Indicates whether Zuul should
1969 auto-generate keys for each project, or whether the test
1970 infrastructure should insert dummy keys to save time during
1971 startup. Defaults to False.
1972
James E. Blaire7b99a02016-08-05 14:27:34 -07001973 The following are instance variables that are useful within test
1974 methods:
1975
1976 :ivar FakeGerritConnection fake_<connection>:
1977 A :py:class:`~tests.base.FakeGerritConnection` will be
1978 instantiated for each connection present in the config file
1979 and stored here. For instance, `fake_gerrit` will hold the
1980 FakeGerritConnection object for a connection named `gerrit`.
1981
1982 :ivar FakeGearmanServer gearman_server: An instance of
1983 :py:class:`~tests.base.FakeGearmanServer` which is the Gearman
1984 server that all of the Zuul components in this test use to
1985 communicate with each other.
1986
Paul Belanger174a8272017-03-14 13:20:10 -04001987 :ivar RecordingExecutorServer executor_server: An instance of
1988 :py:class:`~tests.base.RecordingExecutorServer` which is the
1989 Ansible execute server used to run jobs for this test.
James E. Blaire7b99a02016-08-05 14:27:34 -07001990
1991 :ivar list builds: A list of :py:class:`~tests.base.FakeBuild` objects
1992 representing currently running builds. They are appended to
Paul Belanger174a8272017-03-14 13:20:10 -04001993 the list in the order they are executed, and removed from this
James E. Blaire7b99a02016-08-05 14:27:34 -07001994 list upon completion.
1995
1996 :ivar list history: A list of :py:class:`~tests.base.BuildHistory`
1997 objects representing completed builds. They are appended to
1998 the list in the order they complete.
1999
2000 """
2001
James E. Blair83005782015-12-11 14:46:03 -08002002 config_file = 'zuul.conf'
James E. Blaire1767bc2016-08-02 10:00:27 -07002003 run_ansible = False
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002004 create_project_keys = False
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002005 use_ssl = False
Tobias Henkel054eccc2017-12-20 11:36:19 +01002006 git_url_with_auth = False
James E. Blair3f876d52016-07-22 13:07:14 -07002007
2008 def _startMerger(self):
2009 self.merge_server = zuul.merger.server.MergeServer(self.config,
2010 self.connections)
2011 self.merge_server.start()
2012
Maru Newby3fe5f852015-01-13 04:22:14 +00002013 def setUp(self):
2014 super(ZuulTestCase, self).setUp()
James E. Blair498059b2016-12-20 13:50:13 -08002015
2016 self.setupZK()
2017
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002018 if not KEEP_TEMPDIRS:
James E. Blair97d902e2014-08-21 13:25:56 -07002019 tmp_root = self.useFixture(fixtures.TempDir(
Joshua Hesketh29d99b72014-08-19 16:27:42 +10002020 rootdir=os.environ.get("ZUUL_TEST_ROOT"))
2021 ).path
James E. Blair97d902e2014-08-21 13:25:56 -07002022 else:
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002023 tmp_root = tempfile.mkdtemp(
2024 dir=os.environ.get("ZUUL_TEST_ROOT", None))
Clark Boylanb640e052014-04-03 16:41:46 -07002025 self.test_root = os.path.join(tmp_root, "zuul-test")
2026 self.upstream_root = os.path.join(self.test_root, "upstream")
Monty Taylord642d852017-02-23 14:05:42 -05002027 self.merger_src_root = os.path.join(self.test_root, "merger-git")
Paul Belanger174a8272017-03-14 13:20:10 -04002028 self.executor_src_root = os.path.join(self.test_root, "executor-git")
James E. Blairce8a2132016-05-19 15:21:52 -07002029 self.state_root = os.path.join(self.test_root, "lib")
James E. Blair01d733e2017-06-23 20:47:51 +01002030 self.merger_state_root = os.path.join(self.test_root, "merger-lib")
2031 self.executor_state_root = os.path.join(self.test_root, "executor-lib")
Clark Boylanb640e052014-04-03 16:41:46 -07002032
2033 if os.path.exists(self.test_root):
2034 shutil.rmtree(self.test_root)
2035 os.makedirs(self.test_root)
2036 os.makedirs(self.upstream_root)
James E. Blairce8a2132016-05-19 15:21:52 -07002037 os.makedirs(self.state_root)
James E. Blair01d733e2017-06-23 20:47:51 +01002038 os.makedirs(self.merger_state_root)
2039 os.makedirs(self.executor_state_root)
Clark Boylanb640e052014-04-03 16:41:46 -07002040
2041 # Make per test copy of Configuration.
2042 self.setup_config()
Clint Byrum50c69d82017-05-04 11:55:20 -07002043 self.private_key_file = os.path.join(self.test_root, 'test_id_rsa')
2044 if not os.path.exists(self.private_key_file):
2045 src_private_key_file = os.path.join(FIXTURE_DIR, 'test_id_rsa')
2046 shutil.copy(src_private_key_file, self.private_key_file)
2047 shutil.copy('{}.pub'.format(src_private_key_file),
2048 '{}.pub'.format(self.private_key_file))
2049 os.chmod(self.private_key_file, 0o0600)
James E. Blair39840362017-06-23 20:34:02 +01002050 self.config.set('scheduler', 'tenant_config',
2051 os.path.join(
2052 FIXTURE_DIR,
2053 self.config.get('scheduler', 'tenant_config')))
James E. Blaird1de9462017-06-23 20:53:09 +01002054 self.config.set('scheduler', 'state_dir', self.state_root)
Paul Belanger40d3ce62017-11-28 11:49:55 -05002055 self.config.set(
2056 'scheduler', 'command_socket',
2057 os.path.join(self.test_root, 'scheduler.socket'))
Monty Taylord642d852017-02-23 14:05:42 -05002058 self.config.set('merger', 'git_dir', self.merger_src_root)
Paul Belanger174a8272017-03-14 13:20:10 -04002059 self.config.set('executor', 'git_dir', self.executor_src_root)
Clint Byrum50c69d82017-05-04 11:55:20 -07002060 self.config.set('executor', 'private_key_file', self.private_key_file)
James E. Blair01d733e2017-06-23 20:47:51 +01002061 self.config.set('executor', 'state_dir', self.executor_state_root)
Paul Belanger20920912017-11-28 11:22:30 -05002062 self.config.set(
2063 'executor', 'command_socket',
2064 os.path.join(self.test_root, 'executor.socket'))
James E. Blairda5bb7e2018-01-22 16:12:17 -08002065 self.config.set(
2066 'merger', 'command_socket',
2067 os.path.join(self.test_root, 'merger.socket'))
Clark Boylanb640e052014-04-03 16:41:46 -07002068
Clark Boylanb640e052014-04-03 16:41:46 -07002069 self.statsd = FakeStatsd()
James E. Blairded241e2017-10-10 13:22:40 -07002070 if self.config.has_section('statsd'):
2071 self.config.set('statsd', 'port', str(self.statsd.port))
Clark Boylanb640e052014-04-03 16:41:46 -07002072 self.statsd.start()
Clark Boylanb640e052014-04-03 16:41:46 -07002073
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002074 self.gearman_server = FakeGearmanServer(self.use_ssl)
Clark Boylanb640e052014-04-03 16:41:46 -07002075
2076 self.config.set('gearman', 'port', str(self.gearman_server.port))
James E. Blaire47eb772017-02-02 17:19:40 -08002077 self.log.info("Gearman server on port %s" %
2078 (self.gearman_server.port,))
Paul Belanger0a21f0a2017-06-13 13:14:42 -04002079 if self.use_ssl:
2080 self.log.info('SSL enabled for gearman')
2081 self.config.set(
2082 'gearman', 'ssl_ca',
2083 os.path.join(FIXTURE_DIR, 'gearman/root-ca.pem'))
2084 self.config.set(
2085 'gearman', 'ssl_cert',
2086 os.path.join(FIXTURE_DIR, 'gearman/client.pem'))
2087 self.config.set(
2088 'gearman', 'ssl_key',
2089 os.path.join(FIXTURE_DIR, 'gearman/client.key'))
Clark Boylanb640e052014-04-03 16:41:46 -07002090
Jesse Keating80730e62017-09-14 15:35:11 -06002091 self.rpcclient = zuul.rpcclient.RPCClient(
2092 self.config.get('gearman', 'server'),
2093 self.gearman_server.port,
2094 get_default(self.config, 'gearman', 'ssl_key'),
2095 get_default(self.config, 'gearman', 'ssl_cert'),
2096 get_default(self.config, 'gearman', 'ssl_ca'))
2097
James E. Blaire511d2f2016-12-08 15:22:26 -08002098 gerritsource.GerritSource.replication_timeout = 1.5
2099 gerritsource.GerritSource.replication_retry_interval = 0.5
2100 gerritconnection.GerritEventConnector.delay = 0.0
Clark Boylanb640e052014-04-03 16:41:46 -07002101
Joshua Hesketh352264b2015-08-11 23:42:08 +10002102 self.sched = zuul.scheduler.Scheduler(self.config)
James E. Blairbdd50e62017-10-21 08:18:55 -07002103 self.sched._stats_interval = 1
Clark Boylanb640e052014-04-03 16:41:46 -07002104
Jan Hruban6b71aff2015-10-22 16:58:08 +02002105 self.event_queues = [
2106 self.sched.result_event_queue,
James E. Blair646322f2017-01-27 15:50:34 -08002107 self.sched.trigger_event_queue,
2108 self.sched.management_event_queue
Jan Hruban6b71aff2015-10-22 16:58:08 +02002109 ]
2110
James E. Blairfef78942016-03-11 16:28:56 -08002111 self.configure_connections()
Jesse Keating80730e62017-09-14 15:35:11 -06002112 self.sched.registerConnections(self.connections)
Joshua Hesketh352264b2015-08-11 23:42:08 +10002113
Paul Belanger174a8272017-03-14 13:20:10 -04002114 self.executor_server = RecordingExecutorServer(
James E. Blaira92cbc82017-01-23 14:56:49 -08002115 self.config, self.connections,
James E. Blair854f8892017-02-02 11:25:39 -08002116 jobdir_root=self.test_root,
James E. Blaira92cbc82017-01-23 14:56:49 -08002117 _run_ansible=self.run_ansible,
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002118 _test_root=self.test_root,
2119 keep_jobdir=KEEP_TEMPDIRS)
Paul Belanger174a8272017-03-14 13:20:10 -04002120 self.executor_server.start()
2121 self.history = self.executor_server.build_history
2122 self.builds = self.executor_server.running_builds
James E. Blaire1767bc2016-08-02 10:00:27 -07002123
Paul Belanger174a8272017-03-14 13:20:10 -04002124 self.executor_client = zuul.executor.client.ExecutorClient(
James E. Blair92e953a2017-03-07 13:08:47 -08002125 self.config, self.sched)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002126 self.merge_client = zuul.merger.client.MergeClient(
2127 self.config, self.sched)
James E. Blairda5bb7e2018-01-22 16:12:17 -08002128 self.merge_server = None
James E. Blair8d692392016-04-08 17:47:58 -07002129 self.nodepool = zuul.nodepool.Nodepool(self.sched)
James E. Blairdce6cea2016-12-20 16:45:32 -08002130 self.zk = zuul.zk.ZooKeeper()
James E. Blair0d5a36e2017-02-21 10:53:44 -05002131 self.zk.connect(self.zk_config)
James E. Blairdce6cea2016-12-20 16:45:32 -08002132
James E. Blair0d5a36e2017-02-21 10:53:44 -05002133 self.fake_nodepool = FakeNodepool(
2134 self.zk_chroot_fixture.zookeeper_host,
2135 self.zk_chroot_fixture.zookeeper_port,
2136 self.zk_chroot_fixture.zookeeper_chroot)
Clark Boylanb640e052014-04-03 16:41:46 -07002137
Paul Belanger174a8272017-03-14 13:20:10 -04002138 self.sched.setExecutor(self.executor_client)
Clark Boylanb640e052014-04-03 16:41:46 -07002139 self.sched.setMerger(self.merge_client)
James E. Blair8d692392016-04-08 17:47:58 -07002140 self.sched.setNodepool(self.nodepool)
James E. Blairdce6cea2016-12-20 16:45:32 -08002141 self.sched.setZooKeeper(self.zk)
Clark Boylanb640e052014-04-03 16:41:46 -07002142
Clark Boylanb640e052014-04-03 16:41:46 -07002143 self.sched.start()
Paul Belanger174a8272017-03-14 13:20:10 -04002144 self.executor_client.gearman.waitForServer()
James E. Blaira002b032017-04-18 10:35:48 -07002145 # Cleanups are run in reverse order
2146 self.addCleanup(self.assertCleanShutdown)
Clark Boylanb640e052014-04-03 16:41:46 -07002147 self.addCleanup(self.shutdown)
James E. Blaira002b032017-04-18 10:35:48 -07002148 self.addCleanup(self.assertFinalState)
Clark Boylanb640e052014-04-03 16:41:46 -07002149
James E. Blairb9c0d772017-03-03 14:34:49 -08002150 self.sched.reconfigure(self.config)
2151 self.sched.resume()
2152
Tobias Henkel7df274b2017-05-26 17:41:11 +02002153 def configure_connections(self, source_only=False):
James E. Blaire511d2f2016-12-08 15:22:26 -08002154 # Set up gerrit related fakes
2155 # Set a changes database so multiple FakeGerrit's can report back to
2156 # a virtual canonical database given by the configured hostname
2157 self.gerrit_changes_dbs = {}
James E. Blair6bacffb2018-01-05 13:45:25 -08002158 self.github_changes_dbs = {}
James E. Blaire511d2f2016-12-08 15:22:26 -08002159
2160 def getGerritConnection(driver, name, config):
2161 db = self.gerrit_changes_dbs.setdefault(config['server'], {})
2162 con = FakeGerritConnection(driver, name, config,
2163 changes_db=db,
2164 upstream_root=self.upstream_root)
2165 self.event_queues.append(con.event_queue)
2166 setattr(self, 'fake_' + name, con)
2167 return con
2168
2169 self.useFixture(fixtures.MonkeyPatch(
2170 'zuul.driver.gerrit.GerritDriver.getConnection',
2171 getGerritConnection))
2172
Gregory Haynes4fc12542015-04-22 20:38:06 -07002173 def getGithubConnection(driver, name, config):
James E. Blair6bacffb2018-01-05 13:45:25 -08002174 server = config.get('server', 'github.com')
2175 db = self.github_changes_dbs.setdefault(server, {})
Tobias Henkel054eccc2017-12-20 11:36:19 +01002176 con = FakeGithubConnection(
2177 driver, name, config,
2178 self.rpcclient,
2179 changes_db=db,
2180 upstream_root=self.upstream_root,
2181 git_url_with_auth=self.git_url_with_auth)
Jesse Keating64d29012017-09-06 12:27:49 -07002182 self.event_queues.append(con.event_queue)
Gregory Haynes4fc12542015-04-22 20:38:06 -07002183 setattr(self, 'fake_' + name, con)
2184 return con
2185
2186 self.useFixture(fixtures.MonkeyPatch(
2187 'zuul.driver.github.GithubDriver.getConnection',
2188 getGithubConnection))
2189
James E. Blaire511d2f2016-12-08 15:22:26 -08002190 # Set up smtp related fakes
Joshua Heskethd78b4482015-09-14 16:56:34 -06002191 # TODO(jhesketh): This should come from lib.connections for better
2192 # coverage
Joshua Hesketh352264b2015-08-11 23:42:08 +10002193 # Register connections from the config
2194 self.smtp_messages = []
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002195
Joshua Hesketh352264b2015-08-11 23:42:08 +10002196 def FakeSMTPFactory(*args, **kw):
2197 args = [self.smtp_messages] + list(args)
2198 return FakeSMTP(*args, **kw)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002199
Joshua Hesketh352264b2015-08-11 23:42:08 +10002200 self.useFixture(fixtures.MonkeyPatch('smtplib.SMTP', FakeSMTPFactory))
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002201
James E. Blaire511d2f2016-12-08 15:22:26 -08002202 # Register connections from the config using fakes
James E. Blairfef78942016-03-11 16:28:56 -08002203 self.connections = zuul.lib.connections.ConnectionRegistry()
Tobias Henkel7df274b2017-05-26 17:41:11 +02002204 self.connections.configure(self.config, source_only=source_only)
Joshua Hesketh850ccb62014-11-27 11:31:02 +11002205
James E. Blair83005782015-12-11 14:46:03 -08002206 def setup_config(self):
James E. Blaire7b99a02016-08-05 14:27:34 -07002207 # This creates the per-test configuration object. It can be
2208 # overriden by subclasses, but should not need to be since it
2209 # obeys the config_file and tenant_config_file attributes.
Monty Taylorb934c1a2017-06-16 19:31:47 -05002210 self.config = configparser.ConfigParser()
James E. Blair83005782015-12-11 14:46:03 -08002211 self.config.read(os.path.join(FIXTURE_DIR, self.config_file))
James E. Blair06cc3922017-04-19 10:08:10 -07002212
James E. Blair39840362017-06-23 20:34:02 +01002213 sections = ['zuul', 'scheduler', 'executor', 'merger']
2214 for section in sections:
2215 if not self.config.has_section(section):
2216 self.config.add_section(section)
2217
James E. Blair06cc3922017-04-19 10:08:10 -07002218 if not self.setupSimpleLayout():
2219 if hasattr(self, 'tenant_config_file'):
James E. Blair39840362017-06-23 20:34:02 +01002220 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002221 self.tenant_config_file)
2222 git_path = os.path.join(
2223 os.path.dirname(
2224 os.path.join(FIXTURE_DIR, self.tenant_config_file)),
2225 'git')
2226 if os.path.exists(git_path):
2227 for reponame in os.listdir(git_path):
2228 project = reponame.replace('_', '/')
2229 self.copyDirToRepo(project,
2230 os.path.join(git_path, reponame))
Tristan Cacqueray44aef152017-06-15 06:00:12 +00002231 # Make test_root persist after ansible run for .flag test
Monty Taylor01380dd2017-07-28 16:01:20 -05002232 self.config.set('executor', 'trusted_rw_paths', self.test_root)
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002233 self.setupAllProjectKeys()
2234
James E. Blair06cc3922017-04-19 10:08:10 -07002235 def setupSimpleLayout(self):
2236 # If the test method has been decorated with a simple_layout,
2237 # use that instead of the class tenant_config_file. Set up a
2238 # single config-project with the specified layout, and
2239 # initialize repos for all of the 'project' entries which
2240 # appear in the layout.
2241 test_name = self.id().split('.')[-1]
2242 test = getattr(self, test_name)
2243 if hasattr(test, '__simple_layout__'):
Jesse Keating436a5452017-04-20 11:48:41 -07002244 path, driver = getattr(test, '__simple_layout__')
James E. Blair06cc3922017-04-19 10:08:10 -07002245 else:
2246 return False
2247
James E. Blairb70e55a2017-04-19 12:57:02 -07002248 files = {}
James E. Blair06cc3922017-04-19 10:08:10 -07002249 path = os.path.join(FIXTURE_DIR, path)
2250 with open(path) as f:
James E. Blairb70e55a2017-04-19 12:57:02 -07002251 data = f.read()
2252 layout = yaml.safe_load(data)
2253 files['zuul.yaml'] = data
James E. Blair06cc3922017-04-19 10:08:10 -07002254 untrusted_projects = []
2255 for item in layout:
2256 if 'project' in item:
2257 name = item['project']['name']
2258 untrusted_projects.append(name)
2259 self.init_repo(name)
2260 self.addCommitToRepo(name, 'initial commit',
2261 files={'README': ''},
2262 branch='master', tag='init')
James E. Blairb70e55a2017-04-19 12:57:02 -07002263 if 'job' in item:
James E. Blairb09a0c52017-10-04 07:35:14 -07002264 if 'run' in item['job']:
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002265 files['%s' % item['job']['run']] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002266 for fn in zuul.configloader.as_list(
2267 item['job'].get('pre-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002268 files['%s' % fn] = ''
James E. Blairb09a0c52017-10-04 07:35:14 -07002269 for fn in zuul.configloader.as_list(
2270 item['job'].get('post-run', [])):
Ian Wienand5ede2fa2017-12-05 14:16:19 +11002271 files['%s' % fn] = ''
James E. Blair06cc3922017-04-19 10:08:10 -07002272
2273 root = os.path.join(self.test_root, "config")
2274 if not os.path.exists(root):
2275 os.makedirs(root)
2276 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2277 config = [{'tenant':
2278 {'name': 'tenant-one',
Jesse Keating436a5452017-04-20 11:48:41 -07002279 'source': {driver:
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002280 {'config-projects': ['org/common-config'],
James E. Blair06cc3922017-04-19 10:08:10 -07002281 'untrusted-projects': untrusted_projects}}}}]
Clint Byrumd52b7d72017-05-10 19:40:35 -07002282 f.write(yaml.dump(config).encode('utf8'))
James E. Blair06cc3922017-04-19 10:08:10 -07002283 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002284 self.config.set('scheduler', 'tenant_config',
James E. Blair06cc3922017-04-19 10:08:10 -07002285 os.path.join(FIXTURE_DIR, f.name))
2286
Tobias Henkel3c17d5f2017-08-03 11:46:54 +02002287 self.init_repo('org/common-config')
2288 self.addCommitToRepo('org/common-config', 'add content from fixture',
James E. Blair06cc3922017-04-19 10:08:10 -07002289 files, branch='master', tag='init')
2290
2291 return True
2292
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002293 def setupAllProjectKeys(self):
2294 if self.create_project_keys:
2295 return
2296
James E. Blair39840362017-06-23 20:34:02 +01002297 path = self.config.get('scheduler', 'tenant_config')
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002298 with open(os.path.join(FIXTURE_DIR, path)) as f:
2299 tenant_config = yaml.safe_load(f.read())
2300 for tenant in tenant_config:
2301 sources = tenant['tenant']['source']
2302 for source, conf in sources.items():
James E. Blair109da3f2017-04-04 14:39:43 -07002303 for project in conf.get('config-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002304 self.setupProjectKeys(source, project)
James E. Blair109da3f2017-04-04 14:39:43 -07002305 for project in conf.get('untrusted-projects', []):
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002306 self.setupProjectKeys(source, project)
2307
2308 def setupProjectKeys(self, source, project):
2309 # Make sure we set up an RSA key for the project so that we
2310 # don't spend time generating one:
2311
James E. Blair6459db12017-06-29 14:57:20 -07002312 if isinstance(project, dict):
2313 project = list(project.keys())[0]
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002314 key_root = os.path.join(self.state_root, 'keys')
2315 if not os.path.isdir(key_root):
2316 os.mkdir(key_root, 0o700)
2317 private_key_file = os.path.join(key_root, source, project + '.pem')
2318 private_key_dir = os.path.dirname(private_key_file)
2319 self.log.debug("Installing test keys for project %s at %s" % (
2320 project, private_key_file))
2321 if not os.path.isdir(private_key_dir):
2322 os.makedirs(private_key_dir)
2323 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2324 with open(private_key_file, 'w') as o:
2325 o.write(i.read())
James E. Blair96c6bf82016-01-15 16:20:40 -08002326
James E. Blair498059b2016-12-20 13:50:13 -08002327 def setupZK(self):
Clark Boylan621ec9a2017-04-07 17:41:33 -07002328 self.zk_chroot_fixture = self.useFixture(
2329 ChrootedKazooFixture(self.id()))
James E. Blair0d5a36e2017-02-21 10:53:44 -05002330 self.zk_config = '%s:%s%s' % (
James E. Blairdce6cea2016-12-20 16:45:32 -08002331 self.zk_chroot_fixture.zookeeper_host,
2332 self.zk_chroot_fixture.zookeeper_port,
2333 self.zk_chroot_fixture.zookeeper_chroot)
James E. Blair498059b2016-12-20 13:50:13 -08002334
James E. Blair96c6bf82016-01-15 16:20:40 -08002335 def copyDirToRepo(self, project, source_path):
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002336 self.init_repo(project)
James E. Blair96c6bf82016-01-15 16:20:40 -08002337
2338 files = {}
2339 for (dirpath, dirnames, filenames) in os.walk(source_path):
2340 for filename in filenames:
2341 test_tree_filepath = os.path.join(dirpath, filename)
2342 common_path = os.path.commonprefix([test_tree_filepath,
2343 source_path])
2344 relative_filepath = test_tree_filepath[len(common_path) + 1:]
2345 with open(test_tree_filepath, 'r') as f:
2346 content = f.read()
2347 files[relative_filepath] = content
2348 self.addCommitToRepo(project, 'add content from fixture',
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002349 files, branch='master', tag='init')
James E. Blair83005782015-12-11 14:46:03 -08002350
James E. Blaire18d4602017-01-05 11:17:28 -08002351 def assertNodepoolState(self):
2352 # Make sure that there are no pending requests
2353
2354 requests = self.fake_nodepool.getNodeRequests()
2355 self.assertEqual(len(requests), 0)
2356
2357 nodes = self.fake_nodepool.getNodes()
2358 for node in nodes:
2359 self.assertFalse(node['_lock'], "Node %s is locked" %
2360 (node['_oid'],))
2361
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002362 def assertNoGeneratedKeys(self):
2363 # Make sure that Zuul did not generate any project keys
2364 # (unless it was supposed to).
2365
2366 if self.create_project_keys:
2367 return
2368
2369 with open(os.path.join(FIXTURE_DIR, 'private.pem')) as i:
2370 test_key = i.read()
2371
2372 key_root = os.path.join(self.state_root, 'keys')
2373 for root, dirname, files in os.walk(key_root):
2374 for fn in files:
2375 with open(os.path.join(root, fn)) as f:
2376 self.assertEqual(test_key, f.read())
2377
Clark Boylanb640e052014-04-03 16:41:46 -07002378 def assertFinalState(self):
James E. Blaira002b032017-04-18 10:35:48 -07002379 self.log.debug("Assert final state")
2380 # Make sure no jobs are running
2381 self.assertEqual({}, self.executor_server.job_workers)
Clark Boylanb640e052014-04-03 16:41:46 -07002382 # Make sure that git.Repo objects have been garbage collected.
James E. Blair73b41772017-05-22 13:22:55 -07002383 gc.disable()
Clark Boylanb640e052014-04-03 16:41:46 -07002384 gc.collect()
2385 for obj in gc.get_objects():
2386 if isinstance(obj, git.Repo):
James E. Blair73b41772017-05-22 13:22:55 -07002387 self.log.debug("Leaked git repo object: 0x%x %s" %
2388 (id(obj), repr(obj)))
James E. Blair73b41772017-05-22 13:22:55 -07002389 gc.enable()
Clark Boylanb640e052014-04-03 16:41:46 -07002390 self.assertEmptyQueues()
James E. Blaire18d4602017-01-05 11:17:28 -08002391 self.assertNodepoolState()
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002392 self.assertNoGeneratedKeys()
James E. Blair83005782015-12-11 14:46:03 -08002393 ipm = zuul.manager.independent.IndependentPipelineManager
James E. Blair59fdbac2015-12-07 17:08:06 -08002394 for tenant in self.sched.abide.tenants.values():
2395 for pipeline in tenant.layout.pipelines.values():
James E. Blair83005782015-12-11 14:46:03 -08002396 if isinstance(pipeline.manager, ipm):
James E. Blair59fdbac2015-12-07 17:08:06 -08002397 self.assertEqual(len(pipeline.queues), 0)
Clark Boylanb640e052014-04-03 16:41:46 -07002398
2399 def shutdown(self):
2400 self.log.debug("Shutting down after tests")
James E. Blair5426b112017-05-26 14:19:54 -07002401 self.executor_server.hold_jobs_in_build = False
2402 self.executor_server.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002403 self.executor_client.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002404 self.merge_client.stop()
James E. Blairda5bb7e2018-01-22 16:12:17 -08002405 if self.merge_server:
2406 self.merge_server.stop()
Paul Belanger174a8272017-03-14 13:20:10 -04002407 self.executor_server.stop()
Clark Boylanb640e052014-04-03 16:41:46 -07002408 self.sched.stop()
2409 self.sched.join()
2410 self.statsd.stop()
2411 self.statsd.join()
Jesse Keating80730e62017-09-14 15:35:11 -06002412 self.rpcclient.shutdown()
Clark Boylanb640e052014-04-03 16:41:46 -07002413 self.gearman_server.shutdown()
James E. Blairdce6cea2016-12-20 16:45:32 -08002414 self.fake_nodepool.stop()
2415 self.zk.disconnect()
Clark Boylan8208c192017-04-24 18:08:08 -07002416 self.printHistory()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002417 # We whitelist watchdog threads as they have relatively long delays
Clark Boylanf18e3b82017-04-24 17:34:13 -07002418 # before noticing they should exit, but they should exit on their own.
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002419 # Further the pydevd threads also need to be whitelisted so debugging
2420 # e.g. in PyCharm is possible without breaking shutdown.
James E. Blair7a04df22017-10-17 08:44:52 -07002421 whitelist = ['watchdog',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002422 'pydevd.CommandThread',
2423 'pydevd.Reader',
2424 'pydevd.Writer',
David Shrewsburyfe1f1942017-12-04 13:57:46 -05002425 'socketserver_Thread',
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002426 ]
Clark Boylanf18e3b82017-04-24 17:34:13 -07002427 threads = [t for t in threading.enumerate()
Tobias Henkel9b546cd2017-05-16 09:48:03 +02002428 if t.name not in whitelist]
Clark Boylanb640e052014-04-03 16:41:46 -07002429 if len(threads) > 1:
Clark Boylan8208c192017-04-24 18:08:08 -07002430 log_str = ""
2431 for thread_id, stack_frame in sys._current_frames().items():
2432 log_str += "Thread: %s\n" % thread_id
2433 log_str += "".join(traceback.format_stack(stack_frame))
2434 self.log.debug(log_str)
2435 raise Exception("More than one thread is running: %s" % threads)
Clark Boylanb640e052014-04-03 16:41:46 -07002436
James E. Blaira002b032017-04-18 10:35:48 -07002437 def assertCleanShutdown(self):
2438 pass
2439
James E. Blairc4ba97a2017-04-19 16:26:24 -07002440 def init_repo(self, project, tag=None):
Clark Boylanb640e052014-04-03 16:41:46 -07002441 parts = project.split('/')
2442 path = os.path.join(self.upstream_root, *parts[:-1])
2443 if not os.path.exists(path):
2444 os.makedirs(path)
2445 path = os.path.join(self.upstream_root, project)
2446 repo = git.Repo.init(path)
2447
Morgan Fainberg78c301a2016-07-14 13:47:01 -07002448 with repo.config_writer() as config_writer:
2449 config_writer.set_value('user', 'email', 'user@example.com')
2450 config_writer.set_value('user', 'name', 'User Name')
Clark Boylanb640e052014-04-03 16:41:46 -07002451
Clark Boylanb640e052014-04-03 16:41:46 -07002452 repo.index.commit('initial commit')
2453 master = repo.create_head('master')
James E. Blairc4ba97a2017-04-19 16:26:24 -07002454 if tag:
2455 repo.create_tag(tag)
Clark Boylanb640e052014-04-03 16:41:46 -07002456
James E. Blair97d902e2014-08-21 13:25:56 -07002457 repo.head.reference = master
James E. Blair879dafb2015-07-17 14:04:49 -07002458 zuul.merger.merger.reset_repo_to_head(repo)
James E. Blair97d902e2014-08-21 13:25:56 -07002459 repo.git.clean('-x', '-f', '-d')
2460
James E. Blair97d902e2014-08-21 13:25:56 -07002461 def create_branch(self, project, branch):
2462 path = os.path.join(self.upstream_root, project)
James E. Blairb815c712017-09-22 10:10:19 -07002463 repo = git.Repo(path)
James E. Blair97d902e2014-08-21 13:25:56 -07002464 fn = os.path.join(path, 'README')
2465
2466 branch_head = repo.create_head(branch)
2467 repo.head.reference = branch_head
Clark Boylanb640e052014-04-03 16:41:46 -07002468 f = open(fn, 'a')
James E. Blair97d902e2014-08-21 13:25:56 -07002469 f.write("test %s\n" % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002470 f.close()
2471 repo.index.add([fn])
James E. Blair97d902e2014-08-21 13:25:56 -07002472 repo.index.commit('%s commit' % branch)
Clark Boylanb640e052014-04-03 16:41:46 -07002473
James E. Blair97d902e2014-08-21 13:25:56 -07002474 repo.head.reference = repo.heads['master']
James E. Blair879dafb2015-07-17 14:04:49 -07002475 zuul.merger.merger.reset_repo_to_head(repo)
Clark Boylanb640e052014-04-03 16:41:46 -07002476 repo.git.clean('-x', '-f', '-d')
2477
James E. Blairda5bb7e2018-01-22 16:12:17 -08002478 def delete_branch(self, project, branch):
2479 path = os.path.join(self.upstream_root, project)
2480 repo = git.Repo(path)
2481 repo.head.reference = repo.heads['master']
2482 zuul.merger.merger.reset_repo_to_head(repo)
2483 repo.delete_head(repo.heads[branch], force=True)
2484
Sachi King9f16d522016-03-16 12:20:45 +11002485 def create_commit(self, project):
2486 path = os.path.join(self.upstream_root, project)
2487 repo = git.Repo(path)
2488 repo.head.reference = repo.heads['master']
2489 file_name = os.path.join(path, 'README')
2490 with open(file_name, 'a') as f:
2491 f.write('creating fake commit\n')
2492 repo.index.add([file_name])
2493 commit = repo.index.commit('Creating a fake commit')
2494 return commit.hexsha
2495
James E. Blairf4a5f022017-04-18 14:01:10 -07002496 def orderedRelease(self, count=None):
James E. Blairb8c16472015-05-05 14:55:26 -07002497 # Run one build at a time to ensure non-race order:
James E. Blairf4a5f022017-04-18 14:01:10 -07002498 i = 0
James E. Blairb8c16472015-05-05 14:55:26 -07002499 while len(self.builds):
2500 self.release(self.builds[0])
2501 self.waitUntilSettled()
James E. Blairf4a5f022017-04-18 14:01:10 -07002502 i += 1
2503 if count is not None and i >= count:
2504 break
James E. Blairb8c16472015-05-05 14:55:26 -07002505
James E. Blairdf25ddc2017-07-08 07:57:09 -07002506 def getSortedBuilds(self):
2507 "Return the list of currently running builds sorted by name"
2508
2509 return sorted(self.builds, key=lambda x: x.name)
2510
Clark Boylanb640e052014-04-03 16:41:46 -07002511 def release(self, job):
2512 if isinstance(job, FakeBuild):
2513 job.release()
2514 else:
2515 job.waiting = False
2516 self.log.debug("Queued job %s released" % job.unique)
2517 self.gearman_server.wakeConnections()
2518
2519 def getParameter(self, job, name):
2520 if isinstance(job, FakeBuild):
2521 return job.parameters[name]
2522 else:
2523 parameters = json.loads(job.arguments)
2524 return parameters[name]
2525
Clark Boylanb640e052014-04-03 16:41:46 -07002526 def haveAllBuildsReported(self):
2527 # See if Zuul is waiting on a meta job to complete
Paul Belanger174a8272017-03-14 13:20:10 -04002528 if self.executor_client.meta_jobs:
Clark Boylanb640e052014-04-03 16:41:46 -07002529 return False
2530 # Find out if every build that the worker has completed has been
2531 # reported back to Zuul. If it hasn't then that means a Gearman
2532 # event is still in transit and the system is not stable.
James E. Blair3f876d52016-07-22 13:07:14 -07002533 for build in self.history:
Paul Belanger174a8272017-03-14 13:20:10 -04002534 zbuild = self.executor_client.builds.get(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002535 if not zbuild:
2536 # It has already been reported
2537 continue
2538 # It hasn't been reported yet.
2539 return False
2540 # Make sure that none of the worker connections are in GRAB_WAIT
James E. Blair24c07032017-06-02 15:26:35 -07002541 worker = self.executor_server.executor_worker
2542 for connection in worker.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002543 if connection.state == 'GRAB_WAIT':
2544 return False
2545 return True
2546
2547 def areAllBuildsWaiting(self):
Paul Belanger174a8272017-03-14 13:20:10 -04002548 builds = self.executor_client.builds.values()
James E. Blaira002b032017-04-18 10:35:48 -07002549 seen_builds = set()
Clark Boylanb640e052014-04-03 16:41:46 -07002550 for build in builds:
James E. Blaira002b032017-04-18 10:35:48 -07002551 seen_builds.add(build.uuid)
Clark Boylanb640e052014-04-03 16:41:46 -07002552 client_job = None
Paul Belanger174a8272017-03-14 13:20:10 -04002553 for conn in self.executor_client.gearman.active_connections:
Clark Boylanb640e052014-04-03 16:41:46 -07002554 for j in conn.related_jobs.values():
2555 if j.unique == build.uuid:
2556 client_job = j
2557 break
2558 if not client_job:
2559 self.log.debug("%s is not known to the gearman client" %
2560 build)
James E. Blairf15139b2015-04-02 16:37:15 -07002561 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002562 if not client_job.handle:
2563 self.log.debug("%s has no handle" % client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002564 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002565 server_job = self.gearman_server.jobs.get(client_job.handle)
2566 if not server_job:
2567 self.log.debug("%s is not known to the gearman server" %
2568 client_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002569 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002570 if not hasattr(server_job, 'waiting'):
2571 self.log.debug("%s is being enqueued" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002572 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002573 if server_job.waiting:
2574 continue
James E. Blair17302972016-08-10 16:11:42 -07002575 if build.url is None:
James E. Blairbbda4702016-03-09 15:19:56 -08002576 self.log.debug("%s has not reported start" % build)
2577 return False
Clint Byrumf322fe22017-05-10 20:53:12 -07002578 # using internal ServerJob which offers no Text interface
Paul Belanger174a8272017-03-14 13:20:10 -04002579 worker_build = self.executor_server.job_builds.get(
Clint Byrumf322fe22017-05-10 20:53:12 -07002580 server_job.unique.decode('utf8'))
James E. Blair962220f2016-08-03 11:22:38 -07002581 if worker_build:
2582 if worker_build.isWaiting():
2583 continue
2584 else:
2585 self.log.debug("%s is running" % worker_build)
2586 return False
Clark Boylanb640e052014-04-03 16:41:46 -07002587 else:
James E. Blair962220f2016-08-03 11:22:38 -07002588 self.log.debug("%s is unassigned" % server_job)
James E. Blairf15139b2015-04-02 16:37:15 -07002589 return False
James E. Blaira002b032017-04-18 10:35:48 -07002590 for (build_uuid, job_worker) in \
2591 self.executor_server.job_workers.items():
2592 if build_uuid not in seen_builds:
2593 self.log.debug("%s is not finalized" % build_uuid)
2594 return False
James E. Blairf15139b2015-04-02 16:37:15 -07002595 return True
Clark Boylanb640e052014-04-03 16:41:46 -07002596
James E. Blairdce6cea2016-12-20 16:45:32 -08002597 def areAllNodeRequestsComplete(self):
James E. Blair15be0e12017-01-03 13:45:20 -08002598 if self.fake_nodepool.paused:
2599 return True
James E. Blairdce6cea2016-12-20 16:45:32 -08002600 if self.sched.nodepool.requests:
2601 return False
2602 return True
2603
James E. Blaira615c362017-10-02 17:34:42 -07002604 def areAllMergeJobsWaiting(self):
2605 for client_job in list(self.merge_client.jobs):
2606 if not client_job.handle:
2607 self.log.debug("%s has no handle" % client_job)
2608 return False
2609 server_job = self.gearman_server.jobs.get(client_job.handle)
2610 if not server_job:
2611 self.log.debug("%s is not known to the gearman server" %
2612 client_job)
2613 return False
2614 if not hasattr(server_job, 'waiting'):
2615 self.log.debug("%s is being enqueued" % server_job)
2616 return False
2617 if server_job.waiting:
2618 self.log.debug("%s is waiting" % server_job)
2619 continue
2620 self.log.debug("%s is not waiting" % server_job)
2621 return False
2622 return True
2623
Jan Hruban6b71aff2015-10-22 16:58:08 +02002624 def eventQueuesEmpty(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002625 for event_queue in self.event_queues:
2626 yield event_queue.empty()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002627
2628 def eventQueuesJoin(self):
Monty Taylorb934c1a2017-06-16 19:31:47 -05002629 for event_queue in self.event_queues:
2630 event_queue.join()
Jan Hruban6b71aff2015-10-22 16:58:08 +02002631
Clark Boylanb640e052014-04-03 16:41:46 -07002632 def waitUntilSettled(self):
2633 self.log.debug("Waiting until settled...")
2634 start = time.time()
2635 while True:
Clint Byruma9626572017-02-22 14:04:00 -05002636 if time.time() - start > self.wait_timeout:
James E. Blair10fc1eb2016-12-21 16:16:25 -08002637 self.log.error("Timeout waiting for Zuul to settle")
2638 self.log.error("Queue status:")
Monty Taylorb934c1a2017-06-16 19:31:47 -05002639 for event_queue in self.event_queues:
2640 self.log.error(" %s: %s" %
2641 (event_queue, event_queue.empty()))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002642 self.log.error("All builds waiting: %s" %
James E. Blair622c9682016-06-09 08:14:53 -07002643 (self.areAllBuildsWaiting(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002644 self.log.error("All builds reported: %s" %
James E. Blairf3156c92016-08-10 15:32:19 -07002645 (self.haveAllBuildsReported(),))
James E. Blair10fc1eb2016-12-21 16:16:25 -08002646 self.log.error("All requests completed: %s" %
2647 (self.areAllNodeRequestsComplete(),))
2648 self.log.error("Merge client jobs: %s" %
2649 (self.merge_client.jobs,))
Clark Boylanb640e052014-04-03 16:41:46 -07002650 raise Exception("Timeout waiting for Zuul to settle")
2651 # Make sure no new events show up while we're checking
James E. Blair3f876d52016-07-22 13:07:14 -07002652
Paul Belanger174a8272017-03-14 13:20:10 -04002653 self.executor_server.lock.acquire()
Clark Boylanb640e052014-04-03 16:41:46 -07002654 # have all build states propogated to zuul?
2655 if self.haveAllBuildsReported():
2656 # Join ensures that the queue is empty _and_ events have been
2657 # processed
Jan Hruban6b71aff2015-10-22 16:58:08 +02002658 self.eventQueuesJoin()
Clark Boylanb640e052014-04-03 16:41:46 -07002659 self.sched.run_handler_lock.acquire()
James E. Blaira615c362017-10-02 17:34:42 -07002660 if (self.areAllMergeJobsWaiting() and
Clark Boylanb640e052014-04-03 16:41:46 -07002661 self.haveAllBuildsReported() and
James E. Blairdce6cea2016-12-20 16:45:32 -08002662 self.areAllBuildsWaiting() and
James E. Blair36c611a2017-02-06 15:59:43 -08002663 self.areAllNodeRequestsComplete() and
2664 all(self.eventQueuesEmpty())):
2665 # The queue empty check is placed at the end to
2666 # ensure that if a component adds an event between
2667 # when locked the run handler and checked that the
2668 # components were stable, we don't erroneously
2669 # report that we are settled.
Clark Boylanb640e052014-04-03 16:41:46 -07002670 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002671 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002672 self.log.debug("...settled.")
2673 return
2674 self.sched.run_handler_lock.release()
Paul Belanger174a8272017-03-14 13:20:10 -04002675 self.executor_server.lock.release()
Clark Boylanb640e052014-04-03 16:41:46 -07002676 self.sched.wake_event.wait(0.1)
2677
2678 def countJobResults(self, jobs, result):
2679 jobs = filter(lambda x: x.result == result, jobs)
Clint Byrum1d0c7d12017-05-10 19:40:53 -07002680 return len(list(jobs))
Clark Boylanb640e052014-04-03 16:41:46 -07002681
Monty Taylor0d926122017-05-24 08:07:56 -05002682 def getBuildByName(self, name):
2683 for build in self.builds:
2684 if build.name == name:
2685 return build
2686 raise Exception("Unable to find build %s" % name)
2687
David Shrewsburyf6dc1762017-10-02 13:34:37 -04002688 def assertJobNotInHistory(self, name, project=None):
2689 for job in self.history:
2690 if (project is None or
2691 job.parameters['zuul']['project']['name'] == project):
2692 self.assertNotEqual(job.name, name,
2693 'Job %s found in history' % name)
2694
James E. Blair96c6bf82016-01-15 16:20:40 -08002695 def getJobFromHistory(self, name, project=None):
James E. Blair3f876d52016-07-22 13:07:14 -07002696 for job in self.history:
2697 if (job.name == name and
2698 (project is None or
James E. Blaire5366092017-07-21 15:30:39 -07002699 job.parameters['zuul']['project']['name'] == project)):
James E. Blair3f876d52016-07-22 13:07:14 -07002700 return job
Clark Boylanb640e052014-04-03 16:41:46 -07002701 raise Exception("Unable to find job %s in history" % name)
2702
2703 def assertEmptyQueues(self):
2704 # Make sure there are no orphaned jobs
James E. Blair59fdbac2015-12-07 17:08:06 -08002705 for tenant in self.sched.abide.tenants.values():
2706 for pipeline in tenant.layout.pipelines.values():
Monty Taylorb934c1a2017-06-16 19:31:47 -05002707 for pipeline_queue in pipeline.queues:
2708 if len(pipeline_queue.queue) != 0:
Joshua Hesketh0aa7e8b2016-07-14 00:12:25 +10002709 print('pipeline %s queue %s contents %s' % (
Monty Taylorb934c1a2017-06-16 19:31:47 -05002710 pipeline.name, pipeline_queue.name,
2711 pipeline_queue.queue))
2712 self.assertEqual(len(pipeline_queue.queue), 0,
James E. Blair59fdbac2015-12-07 17:08:06 -08002713 "Pipelines queues should be empty")
Clark Boylanb640e052014-04-03 16:41:46 -07002714
2715 def assertReportedStat(self, key, value=None, kind=None):
2716 start = time.time()
2717 while time.time() < (start + 5):
2718 for stat in self.statsd.stats:
Clint Byrumf322fe22017-05-10 20:53:12 -07002719 k, v = stat.decode('utf-8').split(':')
Clark Boylanb640e052014-04-03 16:41:46 -07002720 if key == k:
2721 if value is None and kind is None:
2722 return
2723 elif value:
2724 if value == v:
2725 return
2726 elif kind:
2727 if v.endswith('|' + kind):
2728 return
2729 time.sleep(0.1)
2730
Clark Boylanb640e052014-04-03 16:41:46 -07002731 raise Exception("Key %s not found in reported stats" % key)
James E. Blair59fdbac2015-12-07 17:08:06 -08002732
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002733 def assertBuilds(self, builds):
2734 """Assert that the running builds are as described.
2735
2736 The list of running builds is examined and must match exactly
2737 the list of builds described by the input.
2738
2739 :arg list builds: A list of dictionaries. Each item in the
2740 list must match the corresponding build in the build
2741 history, and each element of the dictionary must match the
2742 corresponding attribute of the build.
2743
2744 """
James E. Blair3158e282016-08-19 09:34:11 -07002745 try:
2746 self.assertEqual(len(self.builds), len(builds))
2747 for i, d in enumerate(builds):
2748 for k, v in d.items():
2749 self.assertEqual(
2750 getattr(self.builds[i], k), v,
2751 "Element %i in builds does not match" % (i,))
2752 except Exception:
2753 for build in self.builds:
2754 self.log.error("Running build: %s" % build)
2755 else:
2756 self.log.error("No running builds")
2757 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002758
James E. Blairb536ecc2016-08-31 10:11:42 -07002759 def assertHistory(self, history, ordered=True):
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002760 """Assert that the completed builds are as described.
2761
2762 The list of completed builds is examined and must match
2763 exactly the list of builds described by the input.
2764
2765 :arg list history: A list of dictionaries. Each item in the
2766 list must match the corresponding build in the build
2767 history, and each element of the dictionary must match the
2768 corresponding attribute of the build.
2769
James E. Blairb536ecc2016-08-31 10:11:42 -07002770 :arg bool ordered: If true, the history must match the order
2771 supplied, if false, the builds are permitted to have
2772 arrived in any order.
2773
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002774 """
James E. Blairb536ecc2016-08-31 10:11:42 -07002775 def matches(history_item, item):
2776 for k, v in item.items():
2777 if getattr(history_item, k) != v:
2778 return False
2779 return True
James E. Blair3158e282016-08-19 09:34:11 -07002780 try:
2781 self.assertEqual(len(self.history), len(history))
James E. Blairb536ecc2016-08-31 10:11:42 -07002782 if ordered:
2783 for i, d in enumerate(history):
2784 if not matches(self.history[i], d):
2785 raise Exception(
K Jonathan Harkercc3a6f02017-02-22 19:08:06 -08002786 "Element %i in history does not match %s" %
2787 (i, self.history[i]))
James E. Blairb536ecc2016-08-31 10:11:42 -07002788 else:
2789 unseen = self.history[:]
2790 for i, d in enumerate(history):
2791 found = False
2792 for unseen_item in unseen:
2793 if matches(unseen_item, d):
2794 found = True
2795 unseen.remove(unseen_item)
2796 break
2797 if not found:
2798 raise Exception("No match found for element %i "
2799 "in history" % (i,))
2800 if unseen:
2801 raise Exception("Unexpected items in history")
James E. Blair3158e282016-08-19 09:34:11 -07002802 except Exception:
2803 for build in self.history:
2804 self.log.error("Completed build: %s" % build)
2805 else:
2806 self.log.error("No completed builds")
2807 raise
James E. Blair2b2a8ab2016-08-11 14:39:11 -07002808
James E. Blair6ac368c2016-12-22 18:07:20 -08002809 def printHistory(self):
2810 """Log the build history.
2811
2812 This can be useful during tests to summarize what jobs have
2813 completed.
2814
2815 """
2816 self.log.debug("Build history:")
2817 for build in self.history:
2818 self.log.debug(build)
2819
James E. Blair59fdbac2015-12-07 17:08:06 -08002820 def getPipeline(self, name):
James E. Blairf84026c2015-12-08 16:11:46 -08002821 return self.sched.abide.tenants.values()[0].layout.pipelines.get(name)
2822
James E. Blair9ea70072017-04-19 16:05:30 -07002823 def updateConfigLayout(self, path):
James E. Blairf84026c2015-12-08 16:11:46 -08002824 root = os.path.join(self.test_root, "config")
Clint Byrumead6c562017-02-01 16:34:04 -08002825 if not os.path.exists(root):
2826 os.makedirs(root)
James E. Blairf84026c2015-12-08 16:11:46 -08002827 f = tempfile.NamedTemporaryFile(dir=root, delete=False)
2828 f.write("""
Paul Belanger66e95962016-11-11 12:11:06 -05002829- tenant:
2830 name: openstack
2831 source:
2832 gerrit:
James E. Blair109da3f2017-04-04 14:39:43 -07002833 config-projects:
Paul Belanger66e95962016-11-11 12:11:06 -05002834 - %s
James E. Blair109da3f2017-04-04 14:39:43 -07002835 untrusted-projects:
James E. Blair0ffa0102017-03-30 13:11:33 -07002836 - org/project
2837 - org/project1
James E. Blair7cb84542017-04-19 13:35:05 -07002838 - org/project2\n""" % path)
James E. Blairf84026c2015-12-08 16:11:46 -08002839 f.close()
James E. Blair39840362017-06-23 20:34:02 +01002840 self.config.set('scheduler', 'tenant_config',
Paul Belanger66e95962016-11-11 12:11:06 -05002841 os.path.join(FIXTURE_DIR, f.name))
Ricardo Carrillo Cruz22994f92016-12-02 11:41:58 +00002842 self.setupAllProjectKeys()
James E. Blair14abdf42015-12-09 16:11:53 -08002843
Fabien Boucher194a2bf2017-12-02 18:17:58 +01002844 def addTagToRepo(self, project, name, sha):
2845 path = os.path.join(self.upstream_root, project)
2846 repo = git.Repo(path)
2847 repo.git.tag(name, sha)
2848
2849 def delTagFromRepo(self, project, name):
2850 path = os.path.join(self.upstream_root, project)
2851 repo = git.Repo(path)
2852 repo.git.tag('-d', name)
2853
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002854 def addCommitToRepo(self, project, message, files,
2855 branch='master', tag=None):
James E. Blair14abdf42015-12-09 16:11:53 -08002856 path = os.path.join(self.upstream_root, project)
2857 repo = git.Repo(path)
2858 repo.head.reference = branch
2859 zuul.merger.merger.reset_repo_to_head(repo)
2860 for fn, content in files.items():
2861 fn = os.path.join(path, fn)
James E. Blairc73c73a2017-01-20 15:15:15 -08002862 try:
2863 os.makedirs(os.path.dirname(fn))
2864 except OSError:
2865 pass
James E. Blair14abdf42015-12-09 16:11:53 -08002866 with open(fn, 'w') as f:
2867 f.write(content)
2868 repo.index.add([fn])
2869 commit = repo.index.commit(message)
Clint Byrum58264dc2017-02-07 21:21:22 -08002870 before = repo.heads[branch].commit
James E. Blair14abdf42015-12-09 16:11:53 -08002871 repo.heads[branch].commit = commit
2872 repo.head.reference = branch
2873 repo.git.clean('-x', '-f', '-d')
2874 repo.heads[branch].checkout()
James E. Blair8b1dc3f2016-07-05 16:49:00 -07002875 if tag:
2876 repo.create_tag(tag)
Clint Byrum58264dc2017-02-07 21:21:22 -08002877 return before
2878
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002879 def commitConfigUpdate(self, project_name, source_name):
2880 """Commit an update to zuul.yaml
2881
2882 This overwrites the zuul.yaml in the specificed project with
2883 the contents specified.
2884
2885 :arg str project_name: The name of the project containing
2886 zuul.yaml (e.g., common-config)
2887
2888 :arg str source_name: The path to the file (underneath the
2889 test fixture directory) whose contents should be used to
2890 replace zuul.yaml.
2891 """
2892
2893 source_path = os.path.join(FIXTURE_DIR, source_name)
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002894 files = {}
2895 with open(source_path, 'r') as f:
2896 data = f.read()
2897 layout = yaml.safe_load(data)
2898 files['zuul.yaml'] = data
2899 for item in layout:
2900 if 'job' in item:
2901 jobname = item['job']['name']
2902 files['playbooks/%s.yaml' % jobname] = ''
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002903 before = self.addCommitToRepo(
2904 project_name, 'Pulling content from %s' % source_name,
James E. Blairdfdfcfc2017-04-20 10:19:20 -07002905 files)
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002906 return before
2907
Clint Byrum627ba362017-08-14 13:20:40 -07002908 def newTenantConfig(self, source_name):
2909 """ Use this to update the tenant config file in tests
2910
2911 This will update self.tenant_config_file to point to a temporary file
2912 for the duration of this particular test. The content of that file will
2913 be taken from FIXTURE_DIR/source_name
2914
2915 After the test the original value of self.tenant_config_file will be
2916 restored.
2917
2918 :arg str source_name: The path of the file under
2919 FIXTURE_DIR that will be used to populate the new tenant
2920 config file.
2921 """
2922 source_path = os.path.join(FIXTURE_DIR, source_name)
2923 orig_tenant_config_file = self.tenant_config_file
2924 with tempfile.NamedTemporaryFile(
2925 delete=False, mode='wb') as new_tenant_config:
2926 self.tenant_config_file = new_tenant_config.name
2927 with open(source_path, mode='rb') as source_tenant_config:
2928 new_tenant_config.write(source_tenant_config.read())
2929 self.config['scheduler']['tenant_config'] = self.tenant_config_file
2930 self.setupAllProjectKeys()
2931 self.log.debug(
2932 'tenant_config_file = {}'.format(self.tenant_config_file))
2933
2934 def _restoreTenantConfig():
2935 self.log.debug(
2936 'restoring tenant_config_file = {}'.format(
2937 orig_tenant_config_file))
2938 os.unlink(self.tenant_config_file)
2939 self.tenant_config_file = orig_tenant_config_file
2940 self.config['scheduler']['tenant_config'] = orig_tenant_config_file
2941 self.addCleanup(_restoreTenantConfig)
2942
James E. Blair7fc8daa2016-08-08 15:37:15 -07002943 def addEvent(self, connection, event):
James E. Blair9ea0d0b2017-04-20 09:27:15 -07002944
James E. Blair7fc8daa2016-08-08 15:37:15 -07002945 """Inject a Fake (Gerrit) event.
2946
2947 This method accepts a JSON-encoded event and simulates Zuul
2948 having received it from Gerrit. It could (and should)
2949 eventually apply to any connection type, but is currently only
2950 used with Gerrit connections. The name of the connection is
2951 used to look up the corresponding server, and the event is
2952 simulated as having been received by all Zuul connections
2953 attached to that server. So if two Gerrit connections in Zuul
2954 are connected to the same Gerrit server, and you invoke this
2955 method specifying the name of one of them, the event will be
2956 received by both.
2957
2958 .. note::
2959
2960 "self.fake_gerrit.addEvent" calls should be migrated to
2961 this method.
2962
2963 :arg str connection: The name of the connection corresponding
Clark Boylan500992b2017-04-03 14:28:24 -07002964 to the gerrit server.
James E. Blair7fc8daa2016-08-08 15:37:15 -07002965 :arg str event: The JSON-encoded event.
2966
2967 """
2968 specified_conn = self.connections.connections[connection]
2969 for conn in self.connections.connections.values():
2970 if (isinstance(conn, specified_conn.__class__) and
2971 specified_conn.server == conn.server):
2972 conn.addEvent(event)
2973
James E. Blaird8af5422017-05-24 13:59:40 -07002974 def getUpstreamRepos(self, projects):
2975 """Return upstream git repo objects for the listed projects
2976
2977 :arg list projects: A list of strings, each the canonical name
2978 of a project.
2979
2980 :returns: A dictionary of {name: repo} for every listed
2981 project.
2982 :rtype: dict
2983
2984 """
2985
2986 repos = {}
2987 for project in projects:
2988 # FIXME(jeblair): the upstream root does not yet have a
2989 # hostname component; that needs to be added, and this
2990 # line removed:
2991 tmp_project_name = '/'.join(project.split('/')[1:])
2992 path = os.path.join(self.upstream_root, tmp_project_name)
2993 repo = git.Repo(path)
2994 repos[project] = repo
2995 return repos
2996
James E. Blair3f876d52016-07-22 13:07:14 -07002997
2998class AnsibleZuulTestCase(ZuulTestCase):
Paul Belanger174a8272017-03-14 13:20:10 -04002999 """ZuulTestCase but with an actual ansible executor running"""
James E. Blaire1767bc2016-08-02 10:00:27 -07003000 run_ansible = True
Joshua Hesketh25695cb2017-03-06 12:50:04 +11003001
Jamie Lennox7655b552017-03-17 12:33:38 +11003002 @contextmanager
3003 def jobLog(self, build):
3004 """Print job logs on assertion errors
3005
3006 This method is a context manager which, if it encounters an
3007 ecxeption, adds the build log to the debug output.
3008
3009 :arg Build build: The build that's being asserted.
3010 """
3011 try:
3012 yield
3013 except Exception:
3014 path = os.path.join(self.test_root, build.uuid,
3015 'work', 'logs', 'job-output.txt')
3016 with open(path) as f:
3017 self.log.debug(f.read())
3018 raise
3019
Joshua Heskethd78b4482015-09-14 16:56:34 -06003020
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003021class SSLZuulTestCase(ZuulTestCase):
Paul Belangerd3232f52017-06-14 13:54:31 -04003022 """ZuulTestCase but using SSL when possible"""
Paul Belanger0a21f0a2017-06-13 13:14:42 -04003023 use_ssl = True
3024
3025
Joshua Heskethd78b4482015-09-14 16:56:34 -06003026class ZuulDBTestCase(ZuulTestCase):
James E. Blair82844892017-03-06 10:55:26 -08003027 def setup_config(self):
3028 super(ZuulDBTestCase, self).setup_config()
Joshua Heskethd78b4482015-09-14 16:56:34 -06003029 for section_name in self.config.sections():
3030 con_match = re.match(r'^connection ([\'\"]?)(.*)(\1)$',
3031 section_name, re.I)
3032 if not con_match:
3033 continue
3034
3035 if self.config.get(section_name, 'driver') == 'sql':
3036 f = MySQLSchemaFixture()
3037 self.useFixture(f)
3038 if (self.config.get(section_name, 'dburi') ==
3039 '$MYSQL_FIXTURE_DBURI$'):
3040 self.config.set(section_name, 'dburi', f.dburi)