##// END OF EJS Templates
make-config: tweak description - don't call it "bare" and don't reference setup-app
Mads Kiilerich -
r7308:c677d583 default
parent child Browse files
Show More
@@ -1,109 +1,112 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.paster_commands.make_config
15 kallithea.lib.paster_commands.make_config
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 make-config gearbox command for Kallithea
18 make-config gearbox command for Kallithea
19
19
20 :license: GPLv3, see LICENSE.md for more details.
20 :license: GPLv3, see LICENSE.md for more details.
21 """
21 """
22
22
23
23
24 import os
24 import os
25 import sys
25 import sys
26 import uuid
26 import uuid
27 import argparse
27 import argparse
28 from collections import defaultdict
28 from collections import defaultdict
29
29
30 import mako.exceptions
30 import mako.exceptions
31
31
32 from kallithea.lib.paster_commands.common import BasePasterCommand
32 from kallithea.lib.paster_commands.common import BasePasterCommand
33 from kallithea.lib import inifile
33 from kallithea.lib import inifile
34
34
35
35
36 class Command(BasePasterCommand):
36 class Command(BasePasterCommand):
37 """Kallithea: Create a new config file
37 """Kallithea: Create a new config file
38
38
39 make-config is part of a two-phase installation process (the
39 make-config is the first part of the two step setup process. This first
40 second phase is setup-app). make-config creates a bare configuration
40 step creates a customized .ini configuration file. The next step is to run
41 file (possibly filling in defaults from the extra
41 setup-db to populate the database that is referenced in the configuration
42 variables you give).
42 file.
43
43
44 The first key=value arguments are used to customize the Mako variables from
44 The primary high level configuration keys and their default values are
45 what is shown with --show-defaults. Any following key=value arguments will be
45 shown with --show-defaults . Custom values can be specified on the command
46 patched/inserted in the [app:main] section ... until another section name
46 line as key=value arguments when creating a config file.
47 is specified and change where the following values go.
47
48 Additional key=value arguments will be patched/inserted in the [app:main]
49 section ... until another section name specifies where any following values
50 should go.
48 """
51 """
49
52
50 takes_config_file = False # at least not an existing one ...
53 takes_config_file = False # at least not an existing one ...
51
54
52 def take_action(self, args):
55 def take_action(self, args):
53 _run(args)
56 _run(args)
54
57
55 def get_parser(self, prog_name):
58 def get_parser(self, prog_name):
56 parser = super(Command, self).get_parser(prog_name)
59 parser = super(Command, self).get_parser(prog_name)
57
60
58 parser.add_argument('config_file', nargs='?',
61 parser.add_argument('config_file', nargs='?',
59 help='application config file to write')
62 help='application config file to write')
60
63
61 parser.add_argument('custom', nargs=argparse.REMAINDER,
64 parser.add_argument('custom', nargs=argparse.REMAINDER,
62 help='"key=value" for customizing the config file')
65 help='"key=value" for customizing the config file')
63
66
64 parser.add_argument('--show-defaults', action='store_true',
67 parser.add_argument('--show-defaults', action='store_true',
65 help="Show the default values that can be overridden")
68 help="Show the default values that can be overridden")
66
69
67 return parser
70 return parser
68
71
69
72
70 def _run(args):
73 def _run(args):
71 if args.show_defaults:
74 if args.show_defaults:
72 if args.config_file is not None:
75 if args.config_file is not None:
73 raise ValueError("Can't specify both config file and --show-defaults")
76 raise ValueError("Can't specify both config file and --show-defaults")
74 for key, value in inifile.default_variables.items():
77 for key, value in inifile.default_variables.items():
75 print '%s=%s' % (key, value)
78 print '%s=%s' % (key, value)
76 sys.exit(0)
79 sys.exit(0)
77 if args.config_file is None:
80 if args.config_file is None:
78 raise ValueError("Missing argument: config file")
81 raise ValueError("Missing argument: config file")
79
82
80 mako_variable_values = {}
83 mako_variable_values = {}
81 ini_settings = defaultdict(dict)
84 ini_settings = defaultdict(dict)
82
85
83 section_name = None
86 section_name = None
84 for parameter in args.custom:
87 for parameter in args.custom:
85 parts = parameter.split('=', 1)
88 parts = parameter.split('=', 1)
86 if len(parts) == 1 and parameter.startswith('[') and parameter.endswith(']'):
89 if len(parts) == 1 and parameter.startswith('[') and parameter.endswith(']'):
87 section_name = parameter
90 section_name = parameter
88 elif len(parts) == 2:
91 elif len(parts) == 2:
89 key, value = parts
92 key, value = parts
90 if section_name is None and key in inifile.default_variables:
93 if section_name is None and key in inifile.default_variables:
91 mako_variable_values[key] = value
94 mako_variable_values[key] = value
92 else:
95 else:
93 if section_name is None:
96 if section_name is None:
94 section_name = '[app:main]'
97 section_name = '[app:main]'
95 ini_settings[section_name][key] = value
98 ini_settings[section_name][key] = value
96 else:
99 else:
97 raise ValueError("Invalid name=value parameter %r" % parameter)
100 raise ValueError("Invalid name=value parameter %r" % parameter)
98
101
99 # use default that cannot be replaced
102 # use default that cannot be replaced
100 mako_variable_values.update({
103 mako_variable_values.update({
101 'uuid': lambda: uuid.uuid4().hex,
104 'uuid': lambda: uuid.uuid4().hex,
102 })
105 })
103 try:
106 try:
104 config_file = os.path.abspath(args.config_file)
107 config_file = os.path.abspath(args.config_file)
105 inifile.create(config_file, mako_variable_values, ini_settings)
108 inifile.create(config_file, mako_variable_values, ini_settings)
106 print 'Wrote new config file in %s' % config_file
109 print 'Wrote new config file in %s' % config_file
107
110
108 except Exception:
111 except Exception:
109 print mako.exceptions.text_error_template().render()
112 print mako.exceptions.text_error_template().render()
@@ -1,207 +1,204 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 import datetime
15 import datetime
16 import logging
16 import logging
17 import os
17 import os
18 import pytest
18 import pytest
19 import re
19 import re
20 import tempfile
20 import tempfile
21 import time
21 import time
22
22
23 from tg import config
23 from tg import config
24 from webtest import TestApp
24 from webtest import TestApp
25
25
26 from kallithea import model
26 from kallithea import model
27 from kallithea.model.db import Notification, User, UserNotification
27 from kallithea.model.db import Notification, User, UserNotification
28 from kallithea.model.meta import Session
28 from kallithea.model.meta import Session
29 from kallithea.lib.utils2 import safe_str
29 from kallithea.lib.utils2 import safe_str
30
30
31
31
32 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
33
33
34 skipif = pytest.mark.skipif
34 skipif = pytest.mark.skipif
35 parametrize = pytest.mark.parametrize
35 parametrize = pytest.mark.parametrize
36
36
37 # Hack: These module global values MUST be set to actual values before running any tests. This is currently done by conftest.py.
37 # Hack: These module global values MUST be set to actual values before running any tests. This is currently done by conftest.py.
38 url = None
38 url = None
39 testapp = None
39 testapp = None
40
40
41 __all__ = [
41 __all__ = [
42 'skipif', 'parametrize', 'url', 'TestController',
42 'skipif', 'parametrize', 'url', 'TestController',
43 'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
43 'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
44 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
44 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
45 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
45 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
46 'TEST_USER_ADMIN_EMAIL', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
46 'TEST_USER_ADMIN_EMAIL', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
47 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
47 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
48 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
48 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO',
49 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
49 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
50 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO',
50 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO',
51 'GIT_REMOTE_REPO', 'HG_TEST_REVISION', 'GIT_TEST_REVISION',
51 'GIT_REMOTE_REPO', 'HG_TEST_REVISION', 'GIT_TEST_REVISION',
52 ]
52 ]
53
53
54 # Invoke websetup with the current config file
55 # SetupCommand('setup-app').run([config_file])
56
57 ## SOME GLOBALS FOR TESTS
54 ## SOME GLOBALS FOR TESTS
58
55
59 TESTS_TMP_PATH = os.environ.get('KALLITHEA_TESTS_TMP_PATH', tempfile.mkdtemp(prefix='kallithea-test-'))
56 TESTS_TMP_PATH = os.environ.get('KALLITHEA_TESTS_TMP_PATH', tempfile.mkdtemp(prefix='kallithea-test-'))
60
57
61 TEST_USER_ADMIN_LOGIN = 'test_admin'
58 TEST_USER_ADMIN_LOGIN = 'test_admin'
62 TEST_USER_ADMIN_PASS = 'test12'
59 TEST_USER_ADMIN_PASS = 'test12'
63 TEST_USER_ADMIN_EMAIL = 'test_admin@example.com'
60 TEST_USER_ADMIN_EMAIL = 'test_admin@example.com'
64
61
65 TEST_USER_REGULAR_LOGIN = 'test_regular'
62 TEST_USER_REGULAR_LOGIN = 'test_regular'
66 TEST_USER_REGULAR_PASS = 'test12'
63 TEST_USER_REGULAR_PASS = 'test12'
67 TEST_USER_REGULAR_EMAIL = 'test_regular@example.com'
64 TEST_USER_REGULAR_EMAIL = 'test_regular@example.com'
68
65
69 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
66 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
70 TEST_USER_REGULAR2_PASS = 'test12'
67 TEST_USER_REGULAR2_PASS = 'test12'
71 TEST_USER_REGULAR2_EMAIL = 'test_regular2@example.com'
68 TEST_USER_REGULAR2_EMAIL = 'test_regular2@example.com'
72
69
73 HG_REPO = u'vcs_test_hg'
70 HG_REPO = u'vcs_test_hg'
74 GIT_REPO = u'vcs_test_git'
71 GIT_REPO = u'vcs_test_git'
75
72
76 NEW_HG_REPO = u'vcs_test_hg_new'
73 NEW_HG_REPO = u'vcs_test_hg_new'
77 NEW_GIT_REPO = u'vcs_test_git_new'
74 NEW_GIT_REPO = u'vcs_test_git_new'
78
75
79 HG_FORK = u'vcs_test_hg_fork'
76 HG_FORK = u'vcs_test_hg_fork'
80 GIT_FORK = u'vcs_test_git_fork'
77 GIT_FORK = u'vcs_test_git_fork'
81
78
82 HG_TEST_REVISION = u"a53d9201d4bc278910d416d94941b7ea007ecd52"
79 HG_TEST_REVISION = u"a53d9201d4bc278910d416d94941b7ea007ecd52"
83 GIT_TEST_REVISION = u"7ab37bc680b4aa72c34d07b230c866c28e9fc204"
80 GIT_TEST_REVISION = u"7ab37bc680b4aa72c34d07b230c866c28e9fc204"
84
81
85
82
86 ## VCS
83 ## VCS
87 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
84 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
88
85
89 GIT_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
86 GIT_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
90
87
91 TEST_GIT_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
88 TEST_GIT_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
92 TEST_GIT_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-git-clone-%s' % uniq_suffix)
89 TEST_GIT_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-git-clone-%s' % uniq_suffix)
93 TEST_GIT_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-git-pull-%s' % uniq_suffix)
90 TEST_GIT_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-git-pull-%s' % uniq_suffix)
94
91
95 HG_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
92 HG_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
96
93
97 TEST_HG_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
94 TEST_HG_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
98 TEST_HG_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-hg-clone-%s' % uniq_suffix)
95 TEST_HG_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-hg-clone-%s' % uniq_suffix)
99 TEST_HG_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-hg-pull-%s' % uniq_suffix)
96 TEST_HG_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-hg-pull-%s' % uniq_suffix)
100
97
101 # By default, some of the tests will utilise locally available
98 # By default, some of the tests will utilise locally available
102 # repositories stored within tar.gz archives as source for
99 # repositories stored within tar.gz archives as source for
103 # cloning. Should you wish to use some other, remote archive, simply
100 # cloning. Should you wish to use some other, remote archive, simply
104 # uncomment these entries and/or update the URLs to use.
101 # uncomment these entries and/or update the URLs to use.
105 #
102 #
106 # GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
103 # GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
107 # HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
104 # HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
108
105
109 # skip ldap tests if LDAP lib is not installed
106 # skip ldap tests if LDAP lib is not installed
110 ldap_lib_installed = False
107 ldap_lib_installed = False
111 try:
108 try:
112 import ldap
109 import ldap
113 ldap.API_VERSION
110 ldap.API_VERSION
114 ldap_lib_installed = True
111 ldap_lib_installed = True
115 except ImportError:
112 except ImportError:
116 # means that python-ldap is not installed
113 # means that python-ldap is not installed
117 pass
114 pass
118
115
119 try:
116 try:
120 import pam
117 import pam
121 pam.PAM_TEXT_INFO
118 pam.PAM_TEXT_INFO
122 pam_lib_installed = True
119 pam_lib_installed = True
123 except ImportError:
120 except ImportError:
124 pam_lib_installed = False
121 pam_lib_installed = False
125
122
126
123
127 def invalidate_all_caches():
124 def invalidate_all_caches():
128 """Invalidate all beaker caches currently configured.
125 """Invalidate all beaker caches currently configured.
129 Useful when manipulating IP permissions in a test and changes need to take
126 Useful when manipulating IP permissions in a test and changes need to take
130 effect immediately.
127 effect immediately.
131 Note: Any use of this function is probably a workaround - it should be
128 Note: Any use of this function is probably a workaround - it should be
132 replaced with a more specific cache invalidation in code or test."""
129 replaced with a more specific cache invalidation in code or test."""
133 from beaker.cache import cache_managers
130 from beaker.cache import cache_managers
134 for cache in cache_managers.values():
131 for cache in cache_managers.values():
135 cache.clear()
132 cache.clear()
136
133
137
134
138 class NullHandler(logging.Handler):
135 class NullHandler(logging.Handler):
139 def emit(self, record):
136 def emit(self, record):
140 pass
137 pass
141
138
142
139
143 class TestController(object):
140 class TestController(object):
144 """Pytest-style test controller"""
141 """Pytest-style test controller"""
145
142
146 # Note: pytest base classes cannot have an __init__ method
143 # Note: pytest base classes cannot have an __init__ method
147
144
148 @pytest.fixture(autouse=True)
145 @pytest.fixture(autouse=True)
149 def app_fixture(self):
146 def app_fixture(self):
150 h = NullHandler()
147 h = NullHandler()
151 logging.getLogger("kallithea").addHandler(h)
148 logging.getLogger("kallithea").addHandler(h)
152 self.app = TestApp(testapp)
149 self.app = TestApp(testapp)
153 return self.app
150 return self.app
154
151
155 def remove_all_notifications(self):
152 def remove_all_notifications(self):
156 # query().delete() does not (by default) trigger cascades
153 # query().delete() does not (by default) trigger cascades
157 # ( http://docs.sqlalchemy.org/en/rel_0_7/orm/collections.html#passive-deletes )
154 # ( http://docs.sqlalchemy.org/en/rel_0_7/orm/collections.html#passive-deletes )
158 # so delete the UserNotification first to ensure referential integrity.
155 # so delete the UserNotification first to ensure referential integrity.
159 UserNotification.query().delete()
156 UserNotification.query().delete()
160
157
161 Notification.query().delete()
158 Notification.query().delete()
162 Session().commit()
159 Session().commit()
163
160
164 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
161 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
165 password=TEST_USER_ADMIN_PASS):
162 password=TEST_USER_ADMIN_PASS):
166 self._logged_username = username
163 self._logged_username = username
167 response = self.app.post(url(controller='login', action='index'),
164 response = self.app.post(url(controller='login', action='index'),
168 {'username': username,
165 {'username': username,
169 'password': password})
166 'password': password})
170
167
171 if 'Invalid username or password' in response.body:
168 if 'Invalid username or password' in response.body:
172 pytest.fail('could not login using %s %s' % (username, password))
169 pytest.fail('could not login using %s %s' % (username, password))
173
170
174 assert response.status == '302 Found'
171 assert response.status == '302 Found'
175 self.assert_authenticated_user(response, username)
172 self.assert_authenticated_user(response, username)
176
173
177 response = response.follow()
174 response = response.follow()
178 return response.session['authuser']
175 return response.session['authuser']
179
176
180 def _get_logged_user(self):
177 def _get_logged_user(self):
181 return User.get_by_username(self._logged_username)
178 return User.get_by_username(self._logged_username)
182
179
183 def assert_authenticated_user(self, response, expected_username):
180 def assert_authenticated_user(self, response, expected_username):
184 cookie = response.session.get('authuser')
181 cookie = response.session.get('authuser')
185 user = cookie and cookie.get('user_id')
182 user = cookie and cookie.get('user_id')
186 user = user and User.get(user)
183 user = user and User.get(user)
187 user = user and user.username
184 user = user and user.username
188 assert user == expected_username
185 assert user == expected_username
189
186
190 def authentication_token(self):
187 def authentication_token(self):
191 return self.app.get(url('authentication_token')).body
188 return self.app.get(url('authentication_token')).body
192
189
193 def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
190 def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
194 if 'flash' not in response.session:
191 if 'flash' not in response.session:
195 pytest.fail(safe_str(u'msg `%s` not found - session has no flash:\n%s' % (msg, response)))
192 pytest.fail(safe_str(u'msg `%s` not found - session has no flash:\n%s' % (msg, response)))
196 try:
193 try:
197 level, m = response.session['flash'][-1 - skip]
194 level, m = response.session['flash'][-1 - skip]
198 if _matcher(msg, m):
195 if _matcher(msg, m):
199 return
196 return
200 except IndexError:
197 except IndexError:
201 pass
198 pass
202 pytest.fail(safe_str(u'msg `%s` not found in session flash (skipping %s): %s' %
199 pytest.fail(safe_str(u'msg `%s` not found in session flash (skipping %s): %s' %
203 (msg, skip,
200 (msg, skip,
204 ', '.join('`%s`' % m for level, m in response.session['flash']))))
201 ', '.join('`%s`' % m for level, m in response.session['flash']))))
205
202
206 def checkSessionFlashRegex(self, response, regex, skip=0):
203 def checkSessionFlashRegex(self, response, regex, skip=0):
207 self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
204 self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
General Comments 0
You need to be logged in to leave comments. Login now