Skip to content

Commit eb236da

Browse files
author
Vladimir Kotal
authored
Python mirror tool refactor and test (#2851)
fixes #2849
1 parent ff3edb4 commit eb236da

File tree

6 files changed

+172
-74
lines changed

6 files changed

+172
-74
lines changed

dev/before_install

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
3737
elif [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
3838
brew update
3939

40-
brew install cvs
40+
brew install cvs libgit2
4141
if [[ $? != 0 ]]; then
4242
echo "cannot install extra packages"
4343
exit 1

opengrok-tools/setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def my_test_suite():
5555
],
5656
tests_require=[
5757
'pytest',
58+
'pygit2',
5859
],
5960
entry_points={
6061
'console_scripts': [

opengrok-tools/src/main/python/opengrok_tools/mirror.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -106,35 +106,36 @@ def main():
106106
try:
107107
args = parser.parse_args()
108108
except ValueError as e:
109-
fatal(e)
109+
return fatal(e, False)
110110

111111
logger = get_console_logger(get_class_basename(), args.loglevel)
112112

113113
if len(args.project) > 0 and args.all:
114-
fatal("Cannot use both project list and -a/--all")
114+
return fatal("Cannot use both project list and -a/--all", False)
115115

116116
if not args.all and len(args.project) == 0:
117-
fatal("Need at least one project or --all")
117+
return fatal("Need at least one project or --all", False)
118118

119119
if args.config:
120120
config = read_config(logger, args.config)
121121
if config is None:
122-
fatal("Cannot read config file from {}".format(args.config))
122+
return fatal("Cannot read config file from {}".
123+
format(args.config), False)
123124
else:
124125
config = {}
125126

126127
uri = args.uri
127128
if not is_web_uri(uri):
128-
fatal("Not a URI: {}".format(uri))
129+
return fatal("Not a URI: {}".format(uri), False)
129130
logger.debug("web application URI = {}".format(uri))
130131

131132
if not check_configuration(config):
132-
sys.exit(1)
133+
return 1
133134

134135
# Save the source root to avoid querying the web application.
135136
source_root = get_config_value(logger, 'sourceRoot', uri)
136137
if not source_root:
137-
sys.exit(1)
138+
return 1
138139

139140
logger.debug("Source root = {}".format(source_root))
140141

@@ -157,8 +158,8 @@ def main():
157158
if args.batch:
158159
logdir = config.get(LOGDIR_PROPERTY)
159160
if not logdir:
160-
fatal("The {} property is required in batch mode".
161-
format(LOGDIR_PROPERTY))
161+
return fatal("The {} property is required in batch mode".
162+
format(LOGDIR_PROPERTY), False)
162163

163164
projects = args.project
164165
if len(projects) == 1:
@@ -183,19 +184,19 @@ def main():
183184
try:
184185
project_results = pool.map(worker, worker_args, 1)
185186
except KeyboardInterrupt:
186-
sys.exit(FAILURE_EXITVAL)
187+
return FAILURE_EXITVAL
187188
else:
188189
if any([x == FAILURE_EXITVAL for x in project_results]):
189190
ret = FAILURE_EXITVAL
190191
if all([x == CONTINUE_EXITVAL for x in project_results]):
191192
ret = CONTINUE_EXITVAL
192193
except Timeout:
193194
logger.warning("Already running, exiting.")
194-
sys.exit(FAILURE_EXITVAL)
195+
return FAILURE_EXITVAL
195196

196197
logging.shutdown()
197-
sys.exit(ret)
198+
return ret
198199

199200

200201
if __name__ == '__main__':
201-
main()
202+
sys.exit(main())

opengrok-tools/src/main/python/opengrok_tools/utils/log.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,18 @@
3131
)
3232

3333

34-
def fatal(msg):
34+
def fatal(msg, exit=True):
3535
"""
3636
Print message to standard error output and exit
37+
unless the exit parameter is False
3738
:param msg: message
39+
:param exit
3840
"""
3941
print(msg, file=sys.stderr)
40-
sys.exit(FAILURE_EXITVAL)
42+
if exit:
43+
sys.exit(FAILURE_EXITVAL)
44+
else:
45+
return FAILURE_EXITVAL
4146

4247

4348
def add_log_level_argument(parser):

opengrok-tools/src/main/python/opengrok_tools/utils/mirror.py

Lines changed: 89 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,83 @@ def get_project_properties(project_config, project_name, hookdir):
200200
use_proxy, ignored_repos
201201

202202

203+
def process_hook(hook_ident, hook, source_root, project_name, proxy,
204+
hook_timeout):
205+
"""
206+
:param hook_ident: ident of the hook to be used in log entries
207+
:param hook: hook
208+
:param source_root: source root path
209+
:param project_name: project name
210+
:param proxy: proxy or None
211+
:param hook_timeout: hook run timeout
212+
:return: False if hook failed, else True
213+
"""
214+
if hook:
215+
logger = logging.getLogger(__name__)
216+
217+
logger.info("Running {} hook".format(hook_ident))
218+
if run_hook(logger, hook,
219+
os.path.join(source_root, project_name), proxy,
220+
hook_timeout) != SUCCESS_EXITVAL:
221+
logger.error("{} hook failed for project {}".
222+
format(hook_ident, project_name))
223+
return False
224+
225+
return True
226+
227+
228+
def process_changes(repos, project_name, uri):
229+
"""
230+
:param repos: repository list
231+
:param project_name: project name
232+
:return: exit code
233+
"""
234+
logger = logging.getLogger(__name__)
235+
236+
changes_detected = False
237+
238+
# check if the project is a new project - full index is necessary
239+
try:
240+
r = get(logger, get_uri(uri, 'api', 'v1', 'projects',
241+
urllib.parse.quote_plus(project_name),
242+
'property', 'indexed'))
243+
r.raise_for_status()
244+
if not bool(r.json()):
245+
changes_detected = True
246+
logger.info('Project {} has not been indexed yet'
247+
.format(project_name))
248+
except ValueError as e:
249+
logger.error('Unable to parse project \'{}\' indexed flag: {}'
250+
.format(project_name, e))
251+
return FAILURE_EXITVAL
252+
except HTTPError as e:
253+
logger.error('Unable to determine project \'{}\' indexed flag: {}'
254+
.format(project_name, e))
255+
return FAILURE_EXITVAL
256+
257+
# check if the project has any new changes in the SCM
258+
if not changes_detected:
259+
for repo in repos:
260+
try:
261+
if repo.incoming():
262+
logger.debug('Repository {} has incoming changes'.
263+
format(repo))
264+
changes_detected = True
265+
break
266+
except RepositoryException:
267+
logger.error('Cannot determine incoming changes for '
268+
'repository {}'.format(repo))
269+
return FAILURE_EXITVAL
270+
271+
if not changes_detected:
272+
logger.info('No incoming changes for repositories in '
273+
'project {}'.
274+
format(project_name))
275+
return CONTINUE_EXITVAL
276+
277+
return SUCCESS_EXITVAL
278+
279+
203280
def mirror_project(config, project_name, check_changes, uri,
204281
source_root):
205282
"""
@@ -213,6 +290,8 @@ def mirror_project(config, project_name, check_changes, uri,
213290
:return exit code
214291
"""
215292

293+
ret = SUCCESS_EXITVAL
294+
216295
logger = logging.getLogger(__name__)
217296

218297
project_config = get_project_config(config, project_name)
@@ -235,7 +314,6 @@ def mirror_project(config, project_name, check_changes, uri,
235314
# Cache the repositories first. This way it will be known that
236315
# something is not right, avoiding any needless pre-hook run.
237316
#
238-
ret = SUCCESS_EXITVAL
239317
repos = get_repos_for_project(project_name,
240318
ignored_repos,
241319
uri,
@@ -251,59 +329,17 @@ def mirror_project(config, project_name, check_changes, uri,
251329

252330
# Check if the project or any of its repositories have changed.
253331
if check_changes:
254-
changes_detected = False
332+
r = process_changes(repos, project_name, uri)
333+
if r != SUCCESS_EXITVAL:
334+
return r
255335

256-
# check if the project is a new project - full index is necessary
257-
try:
258-
r = get(logger, get_uri(uri, 'api', 'v1', 'projects',
259-
urllib.parse.quote_plus(project_name),
260-
'property', 'indexed'))
261-
r.raise_for_status()
262-
if not bool(r.json()):
263-
changes_detected = True
264-
logger.debug('Project {} has not been indexed yet'
265-
.format(project_name))
266-
except ValueError as e:
267-
logger.error('Unable to parse project \'{}\' indexed flag: {}'
268-
.format(project_name, e))
269-
return FAILURE_EXITVAL
270-
except HTTPError as e:
271-
logger.error('Unable to determine project \'{}\' indexed flag: {}'
272-
.format(project_name, e))
273-
return FAILURE_EXITVAL
274-
275-
# check if the project has any new changes in the SCM
276-
if not changes_detected:
277-
for repo in repos:
278-
try:
279-
if repo.incoming():
280-
logger.debug('Repository {} has incoming changes'.
281-
format(repo))
282-
changes_detected = True
283-
break
284-
except RepositoryException:
285-
logger.error('Cannot determine incoming changes for '
286-
'repository {}'.format(repo))
287-
return FAILURE_EXITVAL
288-
289-
if not changes_detected:
290-
logger.info('No incoming changes for repositories in '
291-
'project {}'.
292-
format(project_name))
293-
return CONTINUE_EXITVAL
294-
295-
if prehook:
296-
logger.info("Running pre hook")
297-
if run_hook(logger, prehook,
298-
os.path.join(source_root, project_name), proxy,
299-
hook_timeout) != SUCCESS_EXITVAL:
300-
logger.error("pre hook failed for project {}".
301-
format(project_name))
302-
return FAILURE_EXITVAL
336+
if not process_hook("pre", prehook, source_root, project_name, proxy,
337+
hook_timeout):
338+
return FAILURE_EXITVAL
303339

304340
#
305341
# If one of the repositories fails to sync, the whole project sync
306-
# is treated as failed, i.e. the program will return 1.
342+
# is treated as failed, i.e. the program will return FAILURE_EXITVAL.
307343
#
308344
for repo in repos:
309345
logger.info("Synchronizing repository {}".
@@ -313,14 +349,9 @@ def mirror_project(config, project_name, check_changes, uri,
313349
format(repo.path))
314350
ret = FAILURE_EXITVAL
315351

316-
if posthook:
317-
logger.info("Running post hook")
318-
if run_hook(logger, posthook,
319-
os.path.join(source_root, project_name), proxy,
320-
hook_timeout) != SUCCESS_EXITVAL:
321-
logger.error("post hook failed for project {}".
322-
format(project_name))
323-
return FAILURE_EXITVAL
352+
if not process_hook("post", posthook, source_root, project_name, proxy,
353+
hook_timeout):
354+
return FAILURE_EXITVAL
324355

325356
return ret
326357

opengrok-tools/src/test/python/test_mirror.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,19 @@
2727
import tempfile
2828
import os
2929
import stat
30+
import pygit2
3031
import pytest
32+
import sys
3133

34+
from opengrok_tools.scm.repofactory import get_repository
3235
from opengrok_tools.utils.mirror import check_project_configuration, \
3336
check_configuration, \
3437
HOOKS_PROPERTY, PROXY_PROPERTY, IGNORED_REPOS_PROPERTY, \
3538
PROJECTS_PROPERTY
39+
import opengrok_tools.mirror
40+
from opengrok_tools.utils.exitvals import (
41+
CONTINUE_EXITVAL,
42+
)
3643

3744

3845
def test_empty_project_configuration():
@@ -109,3 +116,56 @@ def test_valid_config():
109116
assert check_configuration({PROJECTS_PROPERTY:
110117
{"foo": {PROXY_PROPERTY: True}},
111118
PROXY_PROPERTY: "proxy"})
119+
120+
121+
@pytest.mark.skipif(not os.name.startswith("posix"), reason="requires posix")
122+
def test_incoming_retval(monkeypatch):
123+
"""
124+
Test that the special CONTINUE_EXITVAL value bubbles all the way up to
125+
the mirror.py return value.
126+
"""
127+
128+
class MockResponse:
129+
130+
# mock json() method always returns a specific testing dictionary
131+
@staticmethod
132+
def json():
133+
return "true"
134+
135+
@staticmethod
136+
def raise_for_status():
137+
pass
138+
139+
with tempfile.TemporaryDirectory() as source_root:
140+
repo_name = "parent_repo"
141+
repo_path = os.path.join(source_root, repo_name)
142+
cloned_repo_name = "cloned_repo"
143+
cloned_repo_path = os.path.join(source_root, cloned_repo_name)
144+
project_name = "foo" # does not matter for this test
145+
146+
os.mkdir(repo_path)
147+
148+
def mock_get_repos(*args, **kwargs):
149+
return [get_repository(cloned_repo_path,
150+
"git", project_name,
151+
None, None, None, None)]
152+
153+
def mock_get(*args, **kwargs):
154+
return MockResponse()
155+
156+
# Clone a Git repository so that it can pull.
157+
pygit2.init_repository(repo_path, True)
158+
pygit2.clone_repository(repo_path, cloned_repo_path)
159+
160+
with monkeypatch.context() as m:
161+
m.setattr(sys, 'argv', ['prog', "-I", project_name])
162+
163+
# With mocking done via pytest it is necessary to patch
164+
# the call-site rather than the absolute object path.
165+
m.setattr("opengrok_tools.mirror.get_config_value",
166+
lambda x, y, z: source_root)
167+
m.setattr("opengrok_tools.utils.mirror.get_repos_for_project",
168+
mock_get_repos)
169+
m.setattr("opengrok_tools.utils.mirror.get", mock_get)
170+
171+
assert opengrok_tools.mirror.main() == CONTINUE_EXITVAL

0 commit comments

Comments
 (0)