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