##// END OF EJS Templates
tests: Add an autouse fixture to patch the pyro proxy factory.
Martin Bornhold -
r343:73e7f43f default
parent child Browse files
Show More
@@ -1,1601 +1,1622 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import collections
22 22 import datetime
23 23 import hashlib
24 24 import os
25 25 import re
26 26 import pprint
27 27 import shutil
28 28 import socket
29 29 import subprocess
30 30 import time
31 31 import uuid
32 32
33 33 import mock
34 34 import pyramid.testing
35 35 import pytest
36 36 import requests
37 37 from webtest.app import TestApp
38 38
39 39 import rhodecode
40 40 from rhodecode.model.changeset_status import ChangesetStatusModel
41 41 from rhodecode.model.comment import ChangesetCommentsModel
42 42 from rhodecode.model.db import (
43 43 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 44 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 45 from rhodecode.model.meta import Session
46 46 from rhodecode.model.pull_request import PullRequestModel
47 47 from rhodecode.model.repo import RepoModel
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49 from rhodecode.model.user import UserModel
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51 from rhodecode.model.user_group import UserGroupModel
52 52 from rhodecode.lib.utils import repo2db_mapper
53 53 from rhodecode.lib.vcs import create_vcsserver_proxy
54 54 from rhodecode.lib.vcs.backends import get_backend
55 55 from rhodecode.lib.vcs.nodes import FileNode
56 56 from rhodecode.tests import (
57 57 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
58 58 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
59 59 TEST_USER_REGULAR_PASS)
60 60 from rhodecode.tests.fixture import Fixture
61 61
62 62
63 63 def _split_comma(value):
64 64 return value.split(',')
65 65
66 66
67 67 def pytest_addoption(parser):
68 68 parser.addoption(
69 69 '--keep-tmp-path', action='store_true',
70 70 help="Keep the test temporary directories")
71 71 parser.addoption(
72 72 '--backends', action='store', type=_split_comma,
73 73 default=['git', 'hg', 'svn'],
74 74 help="Select which backends to test for backend specific tests.")
75 75 parser.addoption(
76 76 '--dbs', action='store', type=_split_comma,
77 77 default=['sqlite'],
78 78 help="Select which database to test for database specific tests. "
79 79 "Possible options are sqlite,postgres,mysql")
80 80 parser.addoption(
81 81 '--appenlight', '--ae', action='store_true',
82 82 help="Track statistics in appenlight.")
83 83 parser.addoption(
84 84 '--appenlight-api-key', '--ae-key',
85 85 help="API key for Appenlight.")
86 86 parser.addoption(
87 87 '--appenlight-url', '--ae-url',
88 88 default="https://ae.rhodecode.com",
89 89 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
90 90 parser.addoption(
91 91 '--sqlite-connection-string', action='store',
92 92 default='', help="Connection string for the dbs tests with SQLite")
93 93 parser.addoption(
94 94 '--postgres-connection-string', action='store',
95 95 default='', help="Connection string for the dbs tests with Postgres")
96 96 parser.addoption(
97 97 '--mysql-connection-string', action='store',
98 98 default='', help="Connection string for the dbs tests with MySQL")
99 99 parser.addoption(
100 100 '--repeat', type=int, default=100,
101 101 help="Number of repetitions in performance tests.")
102 102
103 103
104 104 def pytest_configure(config):
105 105 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
106 106 from rhodecode.config import patches
107 107 patches.kombu_1_5_1_python_2_7_11()
108 108
109 109
110 110 def pytest_collection_modifyitems(session, config, items):
111 111 # nottest marked, compare nose, used for transition from nose to pytest
112 112 remaining = [
113 113 i for i in items if getattr(i.obj, '__test__', True)]
114 114 items[:] = remaining
115 115
116 116
117 117 def pytest_generate_tests(metafunc):
118 118 # Support test generation based on --backend parameter
119 119 if 'backend_alias' in metafunc.fixturenames:
120 120 backends = get_backends_from_metafunc(metafunc)
121 121 scope = None
122 122 if not backends:
123 123 pytest.skip("Not enabled for any of selected backends")
124 124 metafunc.parametrize('backend_alias', backends, scope=scope)
125 125 elif hasattr(metafunc.function, 'backends'):
126 126 backends = get_backends_from_metafunc(metafunc)
127 127 if not backends:
128 128 pytest.skip("Not enabled for any of selected backends")
129 129
130 130
131 131 def get_backends_from_metafunc(metafunc):
132 132 requested_backends = set(metafunc.config.getoption('--backends'))
133 133 if hasattr(metafunc.function, 'backends'):
134 134 # Supported backends by this test function, created from
135 135 # pytest.mark.backends
136 136 backends = metafunc.function.backends.args
137 137 elif hasattr(metafunc.cls, 'backend_alias'):
138 138 # Support class attribute "backend_alias", this is mainly
139 139 # for legacy reasons for tests not yet using pytest.mark.backends
140 140 backends = [metafunc.cls.backend_alias]
141 141 else:
142 142 backends = metafunc.config.getoption('--backends')
143 143 return requested_backends.intersection(backends)
144 144
145 145
146 146 @pytest.fixture(scope='session', autouse=True)
147 147 def activate_example_rcextensions(request):
148 148 """
149 149 Patch in an example rcextensions module which verifies passed in kwargs.
150 150 """
151 151 from rhodecode.tests.other import example_rcextensions
152 152
153 153 old_extensions = rhodecode.EXTENSIONS
154 154 rhodecode.EXTENSIONS = example_rcextensions
155 155
156 156 @request.addfinalizer
157 157 def cleanup():
158 158 rhodecode.EXTENSIONS = old_extensions
159 159
160 160
161 161 @pytest.fixture
162 162 def capture_rcextensions():
163 163 """
164 164 Returns the recorded calls to entry points in rcextensions.
165 165 """
166 166 calls = rhodecode.EXTENSIONS.calls
167 167 calls.clear()
168 168 # Note: At this moment, it is still the empty dict, but that will
169 169 # be filled during the test run and since it is a reference this
170 170 # is enough to make it work.
171 171 return calls
172 172
173 173
174 174 @pytest.fixture(scope='session')
175 175 def http_environ_session():
176 176 """
177 177 Allow to use "http_environ" in session scope.
178 178 """
179 179 return http_environ(
180 180 http_host_stub=http_host_stub())
181 181
182 182
183 183 @pytest.fixture
184 184 def http_host_stub():
185 185 """
186 186 Value of HTTP_HOST in the test run.
187 187 """
188 188 return 'test.example.com:80'
189 189
190 190
191 191 @pytest.fixture
192 192 def http_environ(http_host_stub):
193 193 """
194 194 HTTP extra environ keys.
195 195
196 196 User by the test application and as well for setting up the pylons
197 197 environment. In the case of the fixture "app" it should be possible
198 198 to override this for a specific test case.
199 199 """
200 200 return {
201 201 'SERVER_NAME': http_host_stub.split(':')[0],
202 202 'SERVER_PORT': http_host_stub.split(':')[1],
203 203 'HTTP_HOST': http_host_stub,
204 204 }
205 205
206 206
207 207 @pytest.fixture(scope='function')
208 208 def app(request, pylonsapp, http_environ):
209 209 app = TestApp(
210 210 pylonsapp,
211 211 extra_environ=http_environ)
212 212 if request.cls:
213 213 request.cls.app = app
214 214 return app
215 215
216 216
217 217 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
218 218
219 219
220 220 def _autologin_user(app, *args):
221 221 session = login_user_session(app, *args)
222 222 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
223 223 return LoginData(csrf_token, session['rhodecode_user'])
224 224
225 225
226 226 @pytest.fixture
227 227 def autologin_user(app):
228 228 """
229 229 Utility fixture which makes sure that the admin user is logged in
230 230 """
231 231 return _autologin_user(app)
232 232
233 233
234 234 @pytest.fixture
235 235 def autologin_regular_user(app):
236 236 """
237 237 Utility fixture which makes sure that the regular user is logged in
238 238 """
239 239 return _autologin_user(
240 240 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
241 241
242 242
243 243 @pytest.fixture(scope='function')
244 244 def csrf_token(request, autologin_user):
245 245 return autologin_user.csrf_token
246 246
247 247
248 248 @pytest.fixture(scope='function')
249 249 def xhr_header(request):
250 250 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
251 251
252 252
253 253 @pytest.fixture
254 254 def real_crypto_backend(monkeypatch):
255 255 """
256 256 Switch the production crypto backend on for this test.
257 257
258 258 During the test run the crypto backend is replaced with a faster
259 259 implementation based on the MD5 algorithm.
260 260 """
261 261 monkeypatch.setattr(rhodecode, 'is_test', False)
262 262
263 263
264 264 @pytest.fixture(scope='class')
265 265 def index_location(request, pylonsapp):
266 266 index_location = pylonsapp.config['app_conf']['search.location']
267 267 if request.cls:
268 268 request.cls.index_location = index_location
269 269 return index_location
270 270
271 271
272 272 @pytest.fixture(scope='session', autouse=True)
273 273 def tests_tmp_path(request):
274 274 """
275 275 Create temporary directory to be used during the test session.
276 276 """
277 277 if not os.path.exists(TESTS_TMP_PATH):
278 278 os.makedirs(TESTS_TMP_PATH)
279 279
280 280 if not request.config.getoption('--keep-tmp-path'):
281 281 @request.addfinalizer
282 282 def remove_tmp_path():
283 283 shutil.rmtree(TESTS_TMP_PATH)
284 284
285 285 return TESTS_TMP_PATH
286 286
287 287
288 @pytest.fixture(scope='session', autouse=True)
289 def patch_pyro_request_scope_proxy_factory(request):
290 """
291 Patch the pyro proxy factory to always use the same dummy request object
292 when under test. This will return the same pyro proxy on every call.
293 """
294 dummy_request = pyramid.testing.DummyRequest()
295
296 def mocked_call(self, request=None):
297 return self.getProxy(request=dummy_request)
298
299 patcher = mock.patch(
300 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
301 new=mocked_call)
302 patcher.start()
303
304 @request.addfinalizer
305 def undo_patching():
306 patcher.stop()
307
308
288 309 @pytest.fixture
289 310 def test_repo_group(request):
290 311 """
291 312 Create a temporary repository group, and destroy it after
292 313 usage automatically
293 314 """
294 315 fixture = Fixture()
295 316 repogroupid = 'test_repo_group_%s' % int(time.time())
296 317 repo_group = fixture.create_repo_group(repogroupid)
297 318
298 319 def _cleanup():
299 320 fixture.destroy_repo_group(repogroupid)
300 321
301 322 request.addfinalizer(_cleanup)
302 323 return repo_group
303 324
304 325
305 326 @pytest.fixture
306 327 def test_user_group(request):
307 328 """
308 329 Create a temporary user group, and destroy it after
309 330 usage automatically
310 331 """
311 332 fixture = Fixture()
312 333 usergroupid = 'test_user_group_%s' % int(time.time())
313 334 user_group = fixture.create_user_group(usergroupid)
314 335
315 336 def _cleanup():
316 337 fixture.destroy_user_group(user_group)
317 338
318 339 request.addfinalizer(_cleanup)
319 340 return user_group
320 341
321 342
322 343 @pytest.fixture(scope='session')
323 344 def test_repo(request):
324 345 container = TestRepoContainer()
325 346 request.addfinalizer(container._cleanup)
326 347 return container
327 348
328 349
329 350 class TestRepoContainer(object):
330 351 """
331 352 Container for test repositories which are used read only.
332 353
333 354 Repositories will be created on demand and re-used during the lifetime
334 355 of this object.
335 356
336 357 Usage to get the svn test repository "minimal"::
337 358
338 359 test_repo = TestContainer()
339 360 repo = test_repo('minimal', 'svn')
340 361
341 362 """
342 363
343 364 dump_extractors = {
344 365 'git': utils.extract_git_repo_from_dump,
345 366 'hg': utils.extract_hg_repo_from_dump,
346 367 'svn': utils.extract_svn_repo_from_dump,
347 368 }
348 369
349 370 def __init__(self):
350 371 self._cleanup_repos = []
351 372 self._fixture = Fixture()
352 373 self._repos = {}
353 374
354 375 def __call__(self, dump_name, backend_alias):
355 376 key = (dump_name, backend_alias)
356 377 if key not in self._repos:
357 378 repo = self._create_repo(dump_name, backend_alias)
358 379 self._repos[key] = repo.repo_id
359 380 return Repository.get(self._repos[key])
360 381
361 382 def _create_repo(self, dump_name, backend_alias):
362 383 repo_name = '%s-%s' % (backend_alias, dump_name)
363 384 backend_class = get_backend(backend_alias)
364 385 dump_extractor = self.dump_extractors[backend_alias]
365 386 repo_path = dump_extractor(dump_name, repo_name)
366 387 vcs_repo = backend_class(repo_path)
367 388 repo2db_mapper({repo_name: vcs_repo})
368 389 repo = RepoModel().get_by_repo_name(repo_name)
369 390 self._cleanup_repos.append(repo_name)
370 391 return repo
371 392
372 393 def _cleanup(self):
373 394 for repo_name in reversed(self._cleanup_repos):
374 395 self._fixture.destroy_repo(repo_name)
375 396
376 397
377 398 @pytest.fixture
378 399 def backend(request, backend_alias, pylonsapp, test_repo):
379 400 """
380 401 Parametrized fixture which represents a single backend implementation.
381 402
382 403 It respects the option `--backends` to focus the test run on specific
383 404 backend implementations.
384 405
385 406 It also supports `pytest.mark.xfail_backends` to mark tests as failing
386 407 for specific backends. This is intended as a utility for incremental
387 408 development of a new backend implementation.
388 409 """
389 410 if backend_alias not in request.config.getoption('--backends'):
390 411 pytest.skip("Backend %s not selected." % (backend_alias, ))
391 412
392 413 utils.check_xfail_backends(request.node, backend_alias)
393 414 utils.check_skip_backends(request.node, backend_alias)
394 415
395 416 repo_name = 'vcs_test_%s' % (backend_alias, )
396 417 backend = Backend(
397 418 alias=backend_alias,
398 419 repo_name=repo_name,
399 420 test_name=request.node.name,
400 421 test_repo_container=test_repo)
401 422 request.addfinalizer(backend.cleanup)
402 423 return backend
403 424
404 425
405 426 @pytest.fixture
406 427 def backend_git(request, pylonsapp, test_repo):
407 428 return backend(request, 'git', pylonsapp, test_repo)
408 429
409 430
410 431 @pytest.fixture
411 432 def backend_hg(request, pylonsapp, test_repo):
412 433 return backend(request, 'hg', pylonsapp, test_repo)
413 434
414 435
415 436 @pytest.fixture
416 437 def backend_svn(request, pylonsapp, test_repo):
417 438 return backend(request, 'svn', pylonsapp, test_repo)
418 439
419 440
420 441 @pytest.fixture
421 442 def backend_random(backend_git):
422 443 """
423 444 Use this to express that your tests need "a backend.
424 445
425 446 A few of our tests need a backend, so that we can run the code. This
426 447 fixture is intended to be used for such cases. It will pick one of the
427 448 backends and run the tests.
428 449
429 450 The fixture `backend` would run the test multiple times for each
430 451 available backend which is a pure waste of time if the test is
431 452 independent of the backend type.
432 453 """
433 454 # TODO: johbo: Change this to pick a random backend
434 455 return backend_git
435 456
436 457
437 458 @pytest.fixture
438 459 def backend_stub(backend_git):
439 460 """
440 461 Use this to express that your tests need a backend stub
441 462
442 463 TODO: mikhail: Implement a real stub logic instead of returning
443 464 a git backend
444 465 """
445 466 return backend_git
446 467
447 468
448 469 @pytest.fixture
449 470 def repo_stub(backend_stub):
450 471 """
451 472 Use this to express that your tests need a repository stub
452 473 """
453 474 return backend_stub.create_repo()
454 475
455 476
456 477 class Backend(object):
457 478 """
458 479 Represents the test configuration for one supported backend
459 480
460 481 Provides easy access to different test repositories based on
461 482 `__getitem__`. Such repositories will only be created once per test
462 483 session.
463 484 """
464 485
465 486 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
466 487 _master_repo = None
467 488 _commit_ids = {}
468 489
469 490 def __init__(self, alias, repo_name, test_name, test_repo_container):
470 491 self.alias = alias
471 492 self.repo_name = repo_name
472 493 self._cleanup_repos = []
473 494 self._test_name = test_name
474 495 self._test_repo_container = test_repo_container
475 496 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
476 497 # Fixture will survive in the end.
477 498 self._fixture = Fixture()
478 499
479 500 def __getitem__(self, key):
480 501 return self._test_repo_container(key, self.alias)
481 502
482 503 @property
483 504 def repo(self):
484 505 """
485 506 Returns the "current" repository. This is the vcs_test repo or the
486 507 last repo which has been created with `create_repo`.
487 508 """
488 509 from rhodecode.model.db import Repository
489 510 return Repository.get_by_repo_name(self.repo_name)
490 511
491 512 @property
492 513 def default_branch_name(self):
493 514 VcsRepository = get_backend(self.alias)
494 515 return VcsRepository.DEFAULT_BRANCH_NAME
495 516
496 517 @property
497 518 def default_head_id(self):
498 519 """
499 520 Returns the default head id of the underlying backend.
500 521
501 522 This will be the default branch name in case the backend does have a
502 523 default branch. In the other cases it will point to a valid head
503 524 which can serve as the base to create a new commit on top of it.
504 525 """
505 526 vcsrepo = self.repo.scm_instance()
506 527 head_id = (
507 528 vcsrepo.DEFAULT_BRANCH_NAME or
508 529 vcsrepo.commit_ids[-1])
509 530 return head_id
510 531
511 532 @property
512 533 def commit_ids(self):
513 534 """
514 535 Returns the list of commits for the last created repository
515 536 """
516 537 return self._commit_ids
517 538
518 539 def create_master_repo(self, commits):
519 540 """
520 541 Create a repository and remember it as a template.
521 542
522 543 This allows to easily create derived repositories to construct
523 544 more complex scenarios for diff, compare and pull requests.
524 545
525 546 Returns a commit map which maps from commit message to raw_id.
526 547 """
527 548 self._master_repo = self.create_repo(commits=commits)
528 549 return self._commit_ids
529 550
530 551 def create_repo(
531 552 self, commits=None, number_of_commits=0, heads=None,
532 553 name_suffix=u'', **kwargs):
533 554 """
534 555 Create a repository and record it for later cleanup.
535 556
536 557 :param commits: Optional. A sequence of dict instances.
537 558 Will add a commit per entry to the new repository.
538 559 :param number_of_commits: Optional. If set to a number, this number of
539 560 commits will be added to the new repository.
540 561 :param heads: Optional. Can be set to a sequence of of commit
541 562 names which shall be pulled in from the master repository.
542 563
543 564 """
544 565 self.repo_name = self._next_repo_name() + name_suffix
545 566 repo = self._fixture.create_repo(
546 567 self.repo_name, repo_type=self.alias, **kwargs)
547 568 self._cleanup_repos.append(repo.repo_name)
548 569
549 570 commits = commits or [
550 571 {'message': 'Commit %s of %s' % (x, self.repo_name)}
551 572 for x in xrange(number_of_commits)]
552 573 self._add_commits_to_repo(repo.scm_instance(), commits)
553 574 if heads:
554 575 self.pull_heads(repo, heads)
555 576
556 577 return repo
557 578
558 579 def pull_heads(self, repo, heads):
559 580 """
560 581 Make sure that repo contains all commits mentioned in `heads`
561 582 """
562 583 vcsmaster = self._master_repo.scm_instance()
563 584 vcsrepo = repo.scm_instance()
564 585 vcsrepo.config.clear_section('hooks')
565 586 commit_ids = [self._commit_ids[h] for h in heads]
566 587 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
567 588
568 589 def create_fork(self):
569 590 repo_to_fork = self.repo_name
570 591 self.repo_name = self._next_repo_name()
571 592 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
572 593 self._cleanup_repos.append(self.repo_name)
573 594 return repo
574 595
575 596 def new_repo_name(self, suffix=u''):
576 597 self.repo_name = self._next_repo_name() + suffix
577 598 self._cleanup_repos.append(self.repo_name)
578 599 return self.repo_name
579 600
580 601 def _next_repo_name(self):
581 602 return u"%s_%s" % (
582 603 self.invalid_repo_name.sub(u'_', self._test_name),
583 604 len(self._cleanup_repos))
584 605
585 606 def ensure_file(self, filename, content='Test content\n'):
586 607 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
587 608 commits = [
588 609 {'added': [
589 610 FileNode(filename, content=content),
590 611 ]},
591 612 ]
592 613 self._add_commits_to_repo(self.repo.scm_instance(), commits)
593 614
594 615 def enable_downloads(self):
595 616 repo = self.repo
596 617 repo.enable_downloads = True
597 618 Session().add(repo)
598 619 Session().commit()
599 620
600 621 def cleanup(self):
601 622 for repo_name in reversed(self._cleanup_repos):
602 623 self._fixture.destroy_repo(repo_name)
603 624
604 625 def _add_commits_to_repo(self, repo, commits):
605 626 if not commits:
606 627 return
607 628
608 629 imc = repo.in_memory_commit
609 630 commit = None
610 631 self._commit_ids = {}
611 632
612 633 for idx, commit in enumerate(commits):
613 634 message = unicode(commit.get('message', 'Commit %s' % idx))
614 635
615 636 for node in commit.get('added', []):
616 637 imc.add(FileNode(node.path, content=node.content))
617 638 for node in commit.get('changed', []):
618 639 imc.change(FileNode(node.path, content=node.content))
619 640 for node in commit.get('removed', []):
620 641 imc.remove(FileNode(node.path))
621 642
622 643 parents = [
623 644 repo.get_commit(commit_id=self._commit_ids[p])
624 645 for p in commit.get('parents', [])]
625 646
626 647 operations = ('added', 'changed', 'removed')
627 648 if not any((commit.get(o) for o in operations)):
628 649 imc.add(FileNode('file_%s' % idx, content=message))
629 650
630 651 commit = imc.commit(
631 652 message=message,
632 653 author=unicode(commit.get('author', 'Automatic')),
633 654 date=commit.get('date'),
634 655 branch=commit.get('branch'),
635 656 parents=parents)
636 657
637 658 self._commit_ids[commit.message] = commit.raw_id
638 659
639 660 # Creating refs for Git to allow fetching them from remote repository
640 661 if self.alias == 'git':
641 662 refs = {}
642 663 for message in self._commit_ids:
643 664 # TODO: mikhail: do more special chars replacements
644 665 ref_name = 'refs/test-refs/{}'.format(
645 666 message.replace(' ', ''))
646 667 refs[ref_name] = self._commit_ids[message]
647 668 self._create_refs(repo, refs)
648 669
649 670 return commit
650 671
651 672 def _create_refs(self, repo, refs):
652 673 for ref_name in refs:
653 674 repo.set_refs(ref_name, refs[ref_name])
654 675
655 676
656 677 @pytest.fixture
657 678 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
658 679 """
659 680 Parametrized fixture which represents a single vcs backend implementation.
660 681
661 682 See the fixture `backend` for more details. This one implements the same
662 683 concept, but on vcs level. So it does not provide model instances etc.
663 684
664 685 Parameters are generated dynamically, see :func:`pytest_generate_tests`
665 686 for how this works.
666 687 """
667 688 if backend_alias not in request.config.getoption('--backends'):
668 689 pytest.skip("Backend %s not selected." % (backend_alias, ))
669 690
670 691 utils.check_xfail_backends(request.node, backend_alias)
671 692 utils.check_skip_backends(request.node, backend_alias)
672 693
673 694 repo_name = 'vcs_test_%s' % (backend_alias, )
674 695 repo_path = os.path.join(tests_tmp_path, repo_name)
675 696 backend = VcsBackend(
676 697 alias=backend_alias,
677 698 repo_path=repo_path,
678 699 test_name=request.node.name,
679 700 test_repo_container=test_repo)
680 701 request.addfinalizer(backend.cleanup)
681 702 return backend
682 703
683 704
684 705 @pytest.fixture
685 706 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
686 707 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
687 708
688 709
689 710 @pytest.fixture
690 711 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
691 712 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
692 713
693 714
694 715 @pytest.fixture
695 716 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
696 717 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
697 718
698 719
699 720 @pytest.fixture
700 721 def vcsbackend_random(vcsbackend_git):
701 722 """
702 723 Use this to express that your tests need "a vcsbackend".
703 724
704 725 The fixture `vcsbackend` would run the test multiple times for each
705 726 available vcs backend which is a pure waste of time if the test is
706 727 independent of the vcs backend type.
707 728 """
708 729 # TODO: johbo: Change this to pick a random backend
709 730 return vcsbackend_git
710 731
711 732
712 733 class VcsBackend(object):
713 734 """
714 735 Represents the test configuration for one supported vcs backend.
715 736 """
716 737
717 738 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
718 739
719 740 def __init__(self, alias, repo_path, test_name, test_repo_container):
720 741 self.alias = alias
721 742 self._repo_path = repo_path
722 743 self._cleanup_repos = []
723 744 self._test_name = test_name
724 745 self._test_repo_container = test_repo_container
725 746
726 747 def __getitem__(self, key):
727 748 return self._test_repo_container(key, self.alias).scm_instance()
728 749
729 750 @property
730 751 def repo(self):
731 752 """
732 753 Returns the "current" repository. This is the vcs_test repo of the last
733 754 repo which has been created.
734 755 """
735 756 Repository = get_backend(self.alias)
736 757 return Repository(self._repo_path)
737 758
738 759 @property
739 760 def backend(self):
740 761 """
741 762 Returns the backend implementation class.
742 763 """
743 764 return get_backend(self.alias)
744 765
745 766 def create_repo(self, number_of_commits=0, _clone_repo=None):
746 767 repo_name = self._next_repo_name()
747 768 self._repo_path = get_new_dir(repo_name)
748 769 Repository = get_backend(self.alias)
749 770 src_url = None
750 771 if _clone_repo:
751 772 src_url = _clone_repo.path
752 773 repo = Repository(self._repo_path, create=True, src_url=src_url)
753 774 self._cleanup_repos.append(repo)
754 775 for idx in xrange(number_of_commits):
755 776 self.ensure_file(filename='file_%s' % idx, content=repo.name)
756 777 return repo
757 778
758 779 def clone_repo(self, repo):
759 780 return self.create_repo(_clone_repo=repo)
760 781
761 782 def cleanup(self):
762 783 for repo in self._cleanup_repos:
763 784 shutil.rmtree(repo.path)
764 785
765 786 def new_repo_path(self):
766 787 repo_name = self._next_repo_name()
767 788 self._repo_path = get_new_dir(repo_name)
768 789 return self._repo_path
769 790
770 791 def _next_repo_name(self):
771 792 return "%s_%s" % (
772 793 self.invalid_repo_name.sub('_', self._test_name),
773 794 len(self._cleanup_repos))
774 795
775 796 def add_file(self, repo, filename, content='Test content\n'):
776 797 imc = repo.in_memory_commit
777 798 imc.add(FileNode(filename, content=content))
778 799 imc.commit(
779 800 message=u'Automatic commit from vcsbackend fixture',
780 801 author=u'Automatic')
781 802
782 803 def ensure_file(self, filename, content='Test content\n'):
783 804 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
784 805 self.add_file(self.repo, filename, content)
785 806
786 807
787 808 @pytest.fixture
788 809 def reposerver(request):
789 810 """
790 811 Allows to serve a backend repository
791 812 """
792 813
793 814 repo_server = RepoServer()
794 815 request.addfinalizer(repo_server.cleanup)
795 816 return repo_server
796 817
797 818
798 819 class RepoServer(object):
799 820 """
800 821 Utility to serve a local repository for the duration of a test case.
801 822
802 823 Supports only Subversion so far.
803 824 """
804 825
805 826 url = None
806 827
807 828 def __init__(self):
808 829 self._cleanup_servers = []
809 830
810 831 def serve(self, vcsrepo):
811 832 if vcsrepo.alias != 'svn':
812 833 raise TypeError("Backend %s not supported" % vcsrepo.alias)
813 834
814 835 proc = subprocess.Popen(
815 836 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
816 837 '--root', vcsrepo.path])
817 838 self._cleanup_servers.append(proc)
818 839 self.url = 'svn://localhost'
819 840
820 841 def cleanup(self):
821 842 for proc in self._cleanup_servers:
822 843 proc.terminate()
823 844
824 845
825 846 @pytest.fixture
826 847 def pr_util(backend, request):
827 848 """
828 849 Utility for tests of models and for functional tests around pull requests.
829 850
830 851 It gives an instance of :class:`PRTestUtility` which provides various
831 852 utility methods around one pull request.
832 853
833 854 This fixture uses `backend` and inherits its parameterization.
834 855 """
835 856
836 857 util = PRTestUtility(backend)
837 858
838 859 @request.addfinalizer
839 860 def cleanup():
840 861 util.cleanup()
841 862
842 863 return util
843 864
844 865
845 866 class PRTestUtility(object):
846 867
847 868 pull_request = None
848 869 pull_request_id = None
849 870 mergeable_patcher = None
850 871 mergeable_mock = None
851 872 notification_patcher = None
852 873
853 874 def __init__(self, backend):
854 875 self.backend = backend
855 876
856 877 def create_pull_request(
857 878 self, commits=None, target_head=None, source_head=None,
858 879 revisions=None, approved=False, author=None, mergeable=False,
859 880 enable_notifications=True, name_suffix=u'', reviewers=None,
860 881 title=u"Test", description=u"Description"):
861 882 self.set_mergeable(mergeable)
862 883 if not enable_notifications:
863 884 # mock notification side effect
864 885 self.notification_patcher = mock.patch(
865 886 'rhodecode.model.notification.NotificationModel.create')
866 887 self.notification_patcher.start()
867 888
868 889 if not self.pull_request:
869 890 if not commits:
870 891 commits = [
871 892 {'message': 'c1'},
872 893 {'message': 'c2'},
873 894 {'message': 'c3'},
874 895 ]
875 896 target_head = 'c1'
876 897 source_head = 'c2'
877 898 revisions = ['c2']
878 899
879 900 self.commit_ids = self.backend.create_master_repo(commits)
880 901 self.target_repository = self.backend.create_repo(
881 902 heads=[target_head], name_suffix=name_suffix)
882 903 self.source_repository = self.backend.create_repo(
883 904 heads=[source_head], name_suffix=name_suffix)
884 905 self.author = author or UserModel().get_by_username(
885 906 TEST_USER_ADMIN_LOGIN)
886 907
887 908 model = PullRequestModel()
888 909 self.create_parameters = {
889 910 'created_by': self.author,
890 911 'source_repo': self.source_repository.repo_name,
891 912 'source_ref': self._default_branch_reference(source_head),
892 913 'target_repo': self.target_repository.repo_name,
893 914 'target_ref': self._default_branch_reference(target_head),
894 915 'revisions': [self.commit_ids[r] for r in revisions],
895 916 'reviewers': reviewers or self._get_reviewers(),
896 917 'title': title,
897 918 'description': description,
898 919 }
899 920 self.pull_request = model.create(**self.create_parameters)
900 921 assert model.get_versions(self.pull_request) == []
901 922
902 923 self.pull_request_id = self.pull_request.pull_request_id
903 924
904 925 if approved:
905 926 self.approve()
906 927
907 928 Session().add(self.pull_request)
908 929 Session().commit()
909 930
910 931 return self.pull_request
911 932
912 933 def approve(self):
913 934 self.create_status_votes(
914 935 ChangesetStatus.STATUS_APPROVED,
915 936 *self.pull_request.reviewers)
916 937
917 938 def close(self):
918 939 PullRequestModel().close_pull_request(self.pull_request, self.author)
919 940
920 941 def _default_branch_reference(self, commit_message):
921 942 reference = '%s:%s:%s' % (
922 943 'branch',
923 944 self.backend.default_branch_name,
924 945 self.commit_ids[commit_message])
925 946 return reference
926 947
927 948 def _get_reviewers(self):
928 949 model = UserModel()
929 950 return [
930 951 model.get_by_username(TEST_USER_REGULAR_LOGIN),
931 952 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
932 953 ]
933 954
934 955 def update_source_repository(self, head=None):
935 956 heads = [head or 'c3']
936 957 self.backend.pull_heads(self.source_repository, heads=heads)
937 958
938 959 def add_one_commit(self, head=None):
939 960 self.update_source_repository(head=head)
940 961 old_commit_ids = set(self.pull_request.revisions)
941 962 PullRequestModel().update_commits(self.pull_request)
942 963 commit_ids = set(self.pull_request.revisions)
943 964 new_commit_ids = commit_ids - old_commit_ids
944 965 assert len(new_commit_ids) == 1
945 966 return new_commit_ids.pop()
946 967
947 968 def remove_one_commit(self):
948 969 assert len(self.pull_request.revisions) == 2
949 970 source_vcs = self.source_repository.scm_instance()
950 971 removed_commit_id = source_vcs.commit_ids[-1]
951 972
952 973 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
953 974 # remove the if once that's sorted out.
954 975 if self.backend.alias == "git":
955 976 kwargs = {'branch_name': self.backend.default_branch_name}
956 977 else:
957 978 kwargs = {}
958 979 source_vcs.strip(removed_commit_id, **kwargs)
959 980
960 981 PullRequestModel().update_commits(self.pull_request)
961 982 assert len(self.pull_request.revisions) == 1
962 983 return removed_commit_id
963 984
964 985 def create_comment(self, linked_to=None):
965 986 comment = ChangesetCommentsModel().create(
966 987 text=u"Test comment",
967 988 repo=self.target_repository.repo_name,
968 989 user=self.author,
969 990 pull_request=self.pull_request)
970 991 assert comment.pull_request_version_id is None
971 992
972 993 if linked_to:
973 994 PullRequestModel()._link_comments_to_version(linked_to)
974 995
975 996 return comment
976 997
977 998 def create_inline_comment(
978 999 self, linked_to=None, line_no=u'n1', file_path='file_1'):
979 1000 comment = ChangesetCommentsModel().create(
980 1001 text=u"Test comment",
981 1002 repo=self.target_repository.repo_name,
982 1003 user=self.author,
983 1004 line_no=line_no,
984 1005 f_path=file_path,
985 1006 pull_request=self.pull_request)
986 1007 assert comment.pull_request_version_id is None
987 1008
988 1009 if linked_to:
989 1010 PullRequestModel()._link_comments_to_version(linked_to)
990 1011
991 1012 return comment
992 1013
993 1014 def create_version_of_pull_request(self):
994 1015 pull_request = self.create_pull_request()
995 1016 version = PullRequestModel()._create_version_from_snapshot(
996 1017 pull_request)
997 1018 return version
998 1019
999 1020 def create_status_votes(self, status, *reviewers):
1000 1021 for reviewer in reviewers:
1001 1022 ChangesetStatusModel().set_status(
1002 1023 repo=self.pull_request.target_repo,
1003 1024 status=status,
1004 1025 user=reviewer.user_id,
1005 1026 pull_request=self.pull_request)
1006 1027
1007 1028 def set_mergeable(self, value):
1008 1029 if not self.mergeable_patcher:
1009 1030 self.mergeable_patcher = mock.patch.object(
1010 1031 VcsSettingsModel, 'get_general_settings')
1011 1032 self.mergeable_mock = self.mergeable_patcher.start()
1012 1033 self.mergeable_mock.return_value = {
1013 1034 'rhodecode_pr_merge_enabled': value}
1014 1035
1015 1036 def cleanup(self):
1016 1037 # In case the source repository is already cleaned up, the pull
1017 1038 # request will already be deleted.
1018 1039 pull_request = PullRequest().get(self.pull_request_id)
1019 1040 if pull_request:
1020 1041 PullRequestModel().delete(pull_request)
1021 1042 Session().commit()
1022 1043
1023 1044 if self.notification_patcher:
1024 1045 self.notification_patcher.stop()
1025 1046
1026 1047 if self.mergeable_patcher:
1027 1048 self.mergeable_patcher.stop()
1028 1049
1029 1050
1030 1051 @pytest.fixture
1031 1052 def user_admin(pylonsapp):
1032 1053 """
1033 1054 Provides the default admin test user as an instance of `db.User`.
1034 1055 """
1035 1056 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1036 1057 return user
1037 1058
1038 1059
1039 1060 @pytest.fixture
1040 1061 def user_regular(pylonsapp):
1041 1062 """
1042 1063 Provides the default regular test user as an instance of `db.User`.
1043 1064 """
1044 1065 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1045 1066 return user
1046 1067
1047 1068
1048 1069 @pytest.fixture
1049 1070 def user_util(request, pylonsapp):
1050 1071 """
1051 1072 Provides a wired instance of `UserUtility` with integrated cleanup.
1052 1073 """
1053 1074 utility = UserUtility(test_name=request.node.name)
1054 1075 request.addfinalizer(utility.cleanup)
1055 1076 return utility
1056 1077
1057 1078
1058 1079 # TODO: johbo: Split this up into utilities per domain or something similar
1059 1080 class UserUtility(object):
1060 1081
1061 1082 def __init__(self, test_name="test"):
1062 1083 self._test_name = test_name
1063 1084 self.fixture = Fixture()
1064 1085 self.repo_group_ids = []
1065 1086 self.user_ids = []
1066 1087 self.user_group_ids = []
1067 1088 self.user_repo_permission_ids = []
1068 1089 self.user_group_repo_permission_ids = []
1069 1090 self.user_repo_group_permission_ids = []
1070 1091 self.user_group_repo_group_permission_ids = []
1071 1092 self.user_user_group_permission_ids = []
1072 1093 self.user_group_user_group_permission_ids = []
1073 1094 self.user_permissions = []
1074 1095
1075 1096 def create_repo_group(
1076 1097 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1077 1098 group_name = "{prefix}_repogroup_{count}".format(
1078 1099 prefix=self._test_name,
1079 1100 count=len(self.repo_group_ids))
1080 1101 repo_group = self.fixture.create_repo_group(
1081 1102 group_name, cur_user=owner)
1082 1103 if auto_cleanup:
1083 1104 self.repo_group_ids.append(repo_group.group_id)
1084 1105 return repo_group
1085 1106
1086 1107 def create_user(self, auto_cleanup=True, **kwargs):
1087 1108 user_name = "{prefix}_user_{count}".format(
1088 1109 prefix=self._test_name,
1089 1110 count=len(self.user_ids))
1090 1111 user = self.fixture.create_user(user_name, **kwargs)
1091 1112 if auto_cleanup:
1092 1113 self.user_ids.append(user.user_id)
1093 1114 return user
1094 1115
1095 1116 def create_user_with_group(self):
1096 1117 user = self.create_user()
1097 1118 user_group = self.create_user_group(members=[user])
1098 1119 return user, user_group
1099 1120
1100 1121 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1101 1122 group_name = "{prefix}_usergroup_{count}".format(
1102 1123 prefix=self._test_name,
1103 1124 count=len(self.user_group_ids))
1104 1125 user_group = self.fixture.create_user_group(group_name, **kwargs)
1105 1126 if auto_cleanup:
1106 1127 self.user_group_ids.append(user_group.users_group_id)
1107 1128 if members:
1108 1129 for user in members:
1109 1130 UserGroupModel().add_user_to_group(user_group, user)
1110 1131 return user_group
1111 1132
1112 1133 def grant_user_permission(self, user_name, permission_name):
1113 1134 self._inherit_default_user_permissions(user_name, False)
1114 1135 self.user_permissions.append((user_name, permission_name))
1115 1136
1116 1137 def grant_user_permission_to_repo_group(
1117 1138 self, repo_group, user, permission_name):
1118 1139 permission = RepoGroupModel().grant_user_permission(
1119 1140 repo_group, user, permission_name)
1120 1141 self.user_repo_group_permission_ids.append(
1121 1142 (repo_group.group_id, user.user_id))
1122 1143 return permission
1123 1144
1124 1145 def grant_user_group_permission_to_repo_group(
1125 1146 self, repo_group, user_group, permission_name):
1126 1147 permission = RepoGroupModel().grant_user_group_permission(
1127 1148 repo_group, user_group, permission_name)
1128 1149 self.user_group_repo_group_permission_ids.append(
1129 1150 (repo_group.group_id, user_group.users_group_id))
1130 1151 return permission
1131 1152
1132 1153 def grant_user_permission_to_repo(
1133 1154 self, repo, user, permission_name):
1134 1155 permission = RepoModel().grant_user_permission(
1135 1156 repo, user, permission_name)
1136 1157 self.user_repo_permission_ids.append(
1137 1158 (repo.repo_id, user.user_id))
1138 1159 return permission
1139 1160
1140 1161 def grant_user_group_permission_to_repo(
1141 1162 self, repo, user_group, permission_name):
1142 1163 permission = RepoModel().grant_user_group_permission(
1143 1164 repo, user_group, permission_name)
1144 1165 self.user_group_repo_permission_ids.append(
1145 1166 (repo.repo_id, user_group.users_group_id))
1146 1167 return permission
1147 1168
1148 1169 def grant_user_permission_to_user_group(
1149 1170 self, target_user_group, user, permission_name):
1150 1171 permission = UserGroupModel().grant_user_permission(
1151 1172 target_user_group, user, permission_name)
1152 1173 self.user_user_group_permission_ids.append(
1153 1174 (target_user_group.users_group_id, user.user_id))
1154 1175 return permission
1155 1176
1156 1177 def grant_user_group_permission_to_user_group(
1157 1178 self, target_user_group, user_group, permission_name):
1158 1179 permission = UserGroupModel().grant_user_group_permission(
1159 1180 target_user_group, user_group, permission_name)
1160 1181 self.user_group_user_group_permission_ids.append(
1161 1182 (target_user_group.users_group_id, user_group.users_group_id))
1162 1183 return permission
1163 1184
1164 1185 def revoke_user_permission(self, user_name, permission_name):
1165 1186 self._inherit_default_user_permissions(user_name, True)
1166 1187 UserModel().revoke_perm(user_name, permission_name)
1167 1188
1168 1189 def _inherit_default_user_permissions(self, user_name, value):
1169 1190 user = UserModel().get_by_username(user_name)
1170 1191 user.inherit_default_permissions = value
1171 1192 Session.add(user)
1172 1193 Session.commit()
1173 1194
1174 1195 def cleanup(self):
1175 1196 self._cleanup_permissions()
1176 1197 self._cleanup_repo_groups()
1177 1198 self._cleanup_user_groups()
1178 1199 self._cleanup_users()
1179 1200
1180 1201 def _cleanup_permissions(self):
1181 1202 if self.user_permissions:
1182 1203 for user_name, permission_name in self.user_permissions:
1183 1204 self.revoke_user_permission(user_name, permission_name)
1184 1205
1185 1206 for permission in self.user_repo_permission_ids:
1186 1207 RepoModel().revoke_user_permission(*permission)
1187 1208
1188 1209 for permission in self.user_group_repo_permission_ids:
1189 1210 RepoModel().revoke_user_group_permission(*permission)
1190 1211
1191 1212 for permission in self.user_repo_group_permission_ids:
1192 1213 RepoGroupModel().revoke_user_permission(*permission)
1193 1214
1194 1215 for permission in self.user_group_repo_group_permission_ids:
1195 1216 RepoGroupModel().revoke_user_group_permission(*permission)
1196 1217
1197 1218 for permission in self.user_user_group_permission_ids:
1198 1219 UserGroupModel().revoke_user_permission(*permission)
1199 1220
1200 1221 for permission in self.user_group_user_group_permission_ids:
1201 1222 UserGroupModel().revoke_user_group_permission(*permission)
1202 1223
1203 1224 def _cleanup_repo_groups(self):
1204 1225 def _repo_group_compare(first_group_id, second_group_id):
1205 1226 """
1206 1227 Gives higher priority to the groups with the most complex paths
1207 1228 """
1208 1229 first_group = RepoGroup.get(first_group_id)
1209 1230 second_group = RepoGroup.get(second_group_id)
1210 1231 first_group_parts = (
1211 1232 len(first_group.group_name.split('/')) if first_group else 0)
1212 1233 second_group_parts = (
1213 1234 len(second_group.group_name.split('/')) if second_group else 0)
1214 1235 return cmp(second_group_parts, first_group_parts)
1215 1236
1216 1237 sorted_repo_group_ids = sorted(
1217 1238 self.repo_group_ids, cmp=_repo_group_compare)
1218 1239 for repo_group_id in sorted_repo_group_ids:
1219 1240 self.fixture.destroy_repo_group(repo_group_id)
1220 1241
1221 1242 def _cleanup_user_groups(self):
1222 1243 def _user_group_compare(first_group_id, second_group_id):
1223 1244 """
1224 1245 Gives higher priority to the groups with the most complex paths
1225 1246 """
1226 1247 first_group = UserGroup.get(first_group_id)
1227 1248 second_group = UserGroup.get(second_group_id)
1228 1249 first_group_parts = (
1229 1250 len(first_group.users_group_name.split('/'))
1230 1251 if first_group else 0)
1231 1252 second_group_parts = (
1232 1253 len(second_group.users_group_name.split('/'))
1233 1254 if second_group else 0)
1234 1255 return cmp(second_group_parts, first_group_parts)
1235 1256
1236 1257 sorted_user_group_ids = sorted(
1237 1258 self.user_group_ids, cmp=_user_group_compare)
1238 1259 for user_group_id in sorted_user_group_ids:
1239 1260 self.fixture.destroy_user_group(user_group_id)
1240 1261
1241 1262 def _cleanup_users(self):
1242 1263 for user_id in self.user_ids:
1243 1264 self.fixture.destroy_user(user_id)
1244 1265
1245 1266
1246 1267 # TODO: Think about moving this into a pytest-pyro package and make it a
1247 1268 # pytest plugin
1248 1269 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1249 1270 def pytest_runtest_makereport(item, call):
1250 1271 """
1251 1272 Adding the remote traceback if the exception has this information.
1252 1273
1253 1274 Pyro4 attaches this information as the attribute `_pyroTraceback`
1254 1275 to the exception instance.
1255 1276 """
1256 1277 outcome = yield
1257 1278 report = outcome.get_result()
1258 1279 if call.excinfo:
1259 1280 _add_pyro_remote_traceback(report, call.excinfo.value)
1260 1281
1261 1282
1262 1283 def _add_pyro_remote_traceback(report, exc):
1263 1284 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1264 1285
1265 1286 if pyro_traceback:
1266 1287 traceback = ''.join(pyro_traceback)
1267 1288 section = 'Pyro4 remote traceback ' + report.when
1268 1289 report.sections.append((section, traceback))
1269 1290
1270 1291
1271 1292 @pytest.fixture(scope='session')
1272 1293 def testrun():
1273 1294 return {
1274 1295 'uuid': uuid.uuid4(),
1275 1296 'start': datetime.datetime.utcnow().isoformat(),
1276 1297 'timestamp': int(time.time()),
1277 1298 }
1278 1299
1279 1300
1280 1301 @pytest.fixture(autouse=True)
1281 1302 def collect_appenlight_stats(request, testrun):
1282 1303 """
1283 1304 This fixture reports memory consumtion of single tests.
1284 1305
1285 1306 It gathers data based on `psutil` and sends them to Appenlight. The option
1286 1307 ``--ae`` has te be used to enable this fixture and the API key for your
1287 1308 application has to be provided in ``--ae-key``.
1288 1309 """
1289 1310 try:
1290 1311 # cygwin cannot have yet psutil support.
1291 1312 import psutil
1292 1313 except ImportError:
1293 1314 return
1294 1315
1295 1316 if not request.config.getoption('--appenlight'):
1296 1317 return
1297 1318 else:
1298 1319 # Only request the pylonsapp fixture if appenlight tracking is
1299 1320 # enabled. This will speed up a test run of unit tests by 2 to 3
1300 1321 # seconds if appenlight is not enabled.
1301 1322 pylonsapp = request.getfuncargvalue("pylonsapp")
1302 1323 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1303 1324 client = AppenlightClient(
1304 1325 url=url,
1305 1326 api_key=request.config.getoption('--appenlight-api-key'),
1306 1327 namespace=request.node.nodeid,
1307 1328 request=str(testrun['uuid']),
1308 1329 testrun=testrun)
1309 1330
1310 1331 client.collect({
1311 1332 'message': "Starting",
1312 1333 })
1313 1334
1314 1335 server_and_port = pylonsapp.config['vcs.server']
1315 1336 server = create_vcsserver_proxy(server_and_port)
1316 1337 with server:
1317 1338 vcs_pid = server.get_pid()
1318 1339 server.run_gc()
1319 1340 vcs_process = psutil.Process(vcs_pid)
1320 1341 mem = vcs_process.memory_info()
1321 1342 client.tag_before('vcsserver.rss', mem.rss)
1322 1343 client.tag_before('vcsserver.vms', mem.vms)
1323 1344
1324 1345 test_process = psutil.Process()
1325 1346 mem = test_process.memory_info()
1326 1347 client.tag_before('test.rss', mem.rss)
1327 1348 client.tag_before('test.vms', mem.vms)
1328 1349
1329 1350 client.tag_before('time', time.time())
1330 1351
1331 1352 @request.addfinalizer
1332 1353 def send_stats():
1333 1354 client.tag_after('time', time.time())
1334 1355 with server:
1335 1356 gc_stats = server.run_gc()
1336 1357 for tag, value in gc_stats.items():
1337 1358 client.tag_after(tag, value)
1338 1359 mem = vcs_process.memory_info()
1339 1360 client.tag_after('vcsserver.rss', mem.rss)
1340 1361 client.tag_after('vcsserver.vms', mem.vms)
1341 1362
1342 1363 mem = test_process.memory_info()
1343 1364 client.tag_after('test.rss', mem.rss)
1344 1365 client.tag_after('test.vms', mem.vms)
1345 1366
1346 1367 client.collect({
1347 1368 'message': "Finished",
1348 1369 })
1349 1370 client.send_stats()
1350 1371
1351 1372 return client
1352 1373
1353 1374
1354 1375 class AppenlightClient():
1355 1376
1356 1377 url_template = '{url}?protocol_version=0.5'
1357 1378
1358 1379 def __init__(
1359 1380 self, url, api_key, add_server=True, add_timestamp=True,
1360 1381 namespace=None, request=None, testrun=None):
1361 1382 self.url = self.url_template.format(url=url)
1362 1383 self.api_key = api_key
1363 1384 self.add_server = add_server
1364 1385 self.add_timestamp = add_timestamp
1365 1386 self.namespace = namespace
1366 1387 self.request = request
1367 1388 self.server = socket.getfqdn(socket.gethostname())
1368 1389 self.tags_before = {}
1369 1390 self.tags_after = {}
1370 1391 self.stats = []
1371 1392 self.testrun = testrun or {}
1372 1393
1373 1394 def tag_before(self, tag, value):
1374 1395 self.tags_before[tag] = value
1375 1396
1376 1397 def tag_after(self, tag, value):
1377 1398 self.tags_after[tag] = value
1378 1399
1379 1400 def collect(self, data):
1380 1401 if self.add_server:
1381 1402 data.setdefault('server', self.server)
1382 1403 if self.add_timestamp:
1383 1404 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1384 1405 if self.namespace:
1385 1406 data.setdefault('namespace', self.namespace)
1386 1407 if self.request:
1387 1408 data.setdefault('request', self.request)
1388 1409 self.stats.append(data)
1389 1410
1390 1411 def send_stats(self):
1391 1412 tags = [
1392 1413 ('testrun', self.request),
1393 1414 ('testrun.start', self.testrun['start']),
1394 1415 ('testrun.timestamp', self.testrun['timestamp']),
1395 1416 ('test', self.namespace),
1396 1417 ]
1397 1418 for key, value in self.tags_before.items():
1398 1419 tags.append((key + '.before', value))
1399 1420 try:
1400 1421 delta = self.tags_after[key] - value
1401 1422 tags.append((key + '.delta', delta))
1402 1423 except Exception:
1403 1424 pass
1404 1425 for key, value in self.tags_after.items():
1405 1426 tags.append((key + '.after', value))
1406 1427 self.collect({
1407 1428 'message': "Collected tags",
1408 1429 'tags': tags,
1409 1430 })
1410 1431
1411 1432 response = requests.post(
1412 1433 self.url,
1413 1434 headers={
1414 1435 'X-appenlight-api-key': self.api_key},
1415 1436 json=self.stats,
1416 1437 )
1417 1438
1418 1439 if not response.status_code == 200:
1419 1440 pprint.pprint(self.stats)
1420 1441 print response.headers
1421 1442 print response.text
1422 1443 raise Exception('Sending to appenlight failed')
1423 1444
1424 1445
1425 1446 @pytest.fixture
1426 1447 def gist_util(request, pylonsapp):
1427 1448 """
1428 1449 Provides a wired instance of `GistUtility` with integrated cleanup.
1429 1450 """
1430 1451 utility = GistUtility()
1431 1452 request.addfinalizer(utility.cleanup)
1432 1453 return utility
1433 1454
1434 1455
1435 1456 class GistUtility(object):
1436 1457 def __init__(self):
1437 1458 self.fixture = Fixture()
1438 1459 self.gist_ids = []
1439 1460
1440 1461 def create_gist(self, **kwargs):
1441 1462 gist = self.fixture.create_gist(**kwargs)
1442 1463 self.gist_ids.append(gist.gist_id)
1443 1464 return gist
1444 1465
1445 1466 def cleanup(self):
1446 1467 for id_ in self.gist_ids:
1447 1468 self.fixture.destroy_gists(str(id_))
1448 1469
1449 1470
1450 1471 @pytest.fixture
1451 1472 def enabled_backends(request):
1452 1473 backends = request.config.option.backends
1453 1474 return backends[:]
1454 1475
1455 1476
1456 1477 @pytest.fixture
1457 1478 def settings_util(request):
1458 1479 """
1459 1480 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1460 1481 """
1461 1482 utility = SettingsUtility()
1462 1483 request.addfinalizer(utility.cleanup)
1463 1484 return utility
1464 1485
1465 1486
1466 1487 class SettingsUtility(object):
1467 1488 def __init__(self):
1468 1489 self.rhodecode_ui_ids = []
1469 1490 self.rhodecode_setting_ids = []
1470 1491 self.repo_rhodecode_ui_ids = []
1471 1492 self.repo_rhodecode_setting_ids = []
1472 1493
1473 1494 def create_repo_rhodecode_ui(
1474 1495 self, repo, section, value, key=None, active=True, cleanup=True):
1475 1496 key = key or hashlib.sha1(
1476 1497 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1477 1498
1478 1499 setting = RepoRhodeCodeUi()
1479 1500 setting.repository_id = repo.repo_id
1480 1501 setting.ui_section = section
1481 1502 setting.ui_value = value
1482 1503 setting.ui_key = key
1483 1504 setting.ui_active = active
1484 1505 Session().add(setting)
1485 1506 Session().commit()
1486 1507
1487 1508 if cleanup:
1488 1509 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1489 1510 return setting
1490 1511
1491 1512 def create_rhodecode_ui(
1492 1513 self, section, value, key=None, active=True, cleanup=True):
1493 1514 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1494 1515
1495 1516 setting = RhodeCodeUi()
1496 1517 setting.ui_section = section
1497 1518 setting.ui_value = value
1498 1519 setting.ui_key = key
1499 1520 setting.ui_active = active
1500 1521 Session().add(setting)
1501 1522 Session().commit()
1502 1523
1503 1524 if cleanup:
1504 1525 self.rhodecode_ui_ids.append(setting.ui_id)
1505 1526 return setting
1506 1527
1507 1528 def create_repo_rhodecode_setting(
1508 1529 self, repo, name, value, type_, cleanup=True):
1509 1530 setting = RepoRhodeCodeSetting(
1510 1531 repo.repo_id, key=name, val=value, type=type_)
1511 1532 Session().add(setting)
1512 1533 Session().commit()
1513 1534
1514 1535 if cleanup:
1515 1536 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1516 1537 return setting
1517 1538
1518 1539 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1519 1540 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1520 1541 Session().add(setting)
1521 1542 Session().commit()
1522 1543
1523 1544 if cleanup:
1524 1545 self.rhodecode_setting_ids.append(setting.app_settings_id)
1525 1546
1526 1547 return setting
1527 1548
1528 1549 def cleanup(self):
1529 1550 for id_ in self.rhodecode_ui_ids:
1530 1551 setting = RhodeCodeUi.get(id_)
1531 1552 Session().delete(setting)
1532 1553
1533 1554 for id_ in self.rhodecode_setting_ids:
1534 1555 setting = RhodeCodeSetting.get(id_)
1535 1556 Session().delete(setting)
1536 1557
1537 1558 for id_ in self.repo_rhodecode_ui_ids:
1538 1559 setting = RepoRhodeCodeUi.get(id_)
1539 1560 Session().delete(setting)
1540 1561
1541 1562 for id_ in self.repo_rhodecode_setting_ids:
1542 1563 setting = RepoRhodeCodeSetting.get(id_)
1543 1564 Session().delete(setting)
1544 1565
1545 1566 Session().commit()
1546 1567
1547 1568
1548 1569 @pytest.fixture
1549 1570 def no_notifications(request):
1550 1571 notification_patcher = mock.patch(
1551 1572 'rhodecode.model.notification.NotificationModel.create')
1552 1573 notification_patcher.start()
1553 1574 request.addfinalizer(notification_patcher.stop)
1554 1575
1555 1576
1556 1577 @pytest.fixture
1557 1578 def silence_action_logger(request):
1558 1579 notification_patcher = mock.patch(
1559 1580 'rhodecode.lib.utils.action_logger')
1560 1581 notification_patcher.start()
1561 1582 request.addfinalizer(notification_patcher.stop)
1562 1583
1563 1584
1564 1585 @pytest.fixture(scope='session')
1565 1586 def repeat(request):
1566 1587 """
1567 1588 The number of repetitions is based on this fixture.
1568 1589
1569 1590 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1570 1591 tests are not too slow in our default test suite.
1571 1592 """
1572 1593 return request.config.getoption('--repeat')
1573 1594
1574 1595
1575 1596 @pytest.fixture
1576 1597 def rhodecode_fixtures():
1577 1598 return Fixture()
1578 1599
1579 1600
1580 1601 @pytest.fixture
1581 1602 def request_stub():
1582 1603 """
1583 1604 Stub request object.
1584 1605 """
1585 1606 request = pyramid.testing.DummyRequest()
1586 1607 request.scheme = 'https'
1587 1608 return request
1588 1609
1589 1610
1590 1611 @pytest.fixture
1591 1612 def config_stub(request, request_stub):
1592 1613 """
1593 1614 Set up pyramid.testing and return the Configurator.
1594 1615 """
1595 1616 config = pyramid.testing.setUp(request=request_stub)
1596 1617
1597 1618 @request.addfinalizer
1598 1619 def cleanup():
1599 1620 pyramid.testing.tearDown()
1600 1621
1601 1622 return config
General Comments 0
You need to be logged in to leave comments. Login now