Skip to content
Snippets Groups Projects
Commit f549a9e0 authored by Benjamin Schubert's avatar Benjamin Schubert
Browse files

Fix os.rename in git source element to correctly handle error codes

According to the documentation
(https://www.unix.com/man-page/POSIX/3posix/rename/), when the directory
already is there, either EEXIST or ENOTEMPTY could be thrown.

Previously only ENOTEMPTY was checked.

Done:
  - Separated the move into its own function
  - Check for both errors
  - Create unit tests for it, covering most test cases
parent 1778e69e
No related branches found
No related tags found
No related merge requests found
...@@ -141,21 +141,24 @@ class GitMirror(SourceFetcher): ...@@ -141,21 +141,24 @@ class GitMirror(SourceFetcher):
fail="Failed to clone git repository {}".format(url), fail="Failed to clone git repository {}".format(url),
fail_temporarily=True) fail_temporarily=True)
# Attempt atomic rename into destination, this will fail if self._atomic_move_mirror(tmpdir, url)
# another process beat us to the punch
try: def _atomic_move_mirror(self, tmpdir, url):
os.rename(tmpdir, self.mirror) # Attempt atomic rename into destination, this will fail if
except OSError as e: # another process beat us to the punch
try:
# When renaming and the destination repo already exists, os.rename() os.rename(tmpdir, self.mirror)
# will fail with ENOTEMPTY, since an empty directory will be silently except OSError as e:
# replaced # When renaming and the destination repo already exists, os.rename()
if e.errno == errno.ENOTEMPTY: # will fail with either ENOTEMPTY or EEXIST, depending on the underlying
self.source.status("{}: Discarding duplicate clone of {}" # implementation.
.format(self.source, url)) # An empty directory would always be replaced.
else: if e.errno in (errno.EEXIST, errno.ENOTEMPTY):
raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}" self.source.status("{}: Discarding duplicate clone of {}"
.format(self.source, url, tmpdir, self.mirror, e)) from e .format(self.source, url))
else:
raise SourceError("{}: Failed to move cloned git repository {} from '{}' to '{}': {}"
.format(self.source, url, tmpdir, self.mirror, e)) from e
def _fetch(self, alias_override=None): def _fetch(self, alias_override=None):
url = self.source.translate_url(self.url, url = self.source.translate_url(self.url,
......
...@@ -21,11 +21,14 @@ ...@@ -21,11 +21,14 @@
# #
import os import os
from unittest import mock
import pytest import pytest
import py.path
from buildstream._exceptions import ErrorDomain from buildstream._exceptions import ErrorDomain
from buildstream import _yaml from buildstream import _yaml, SourceError
from buildstream.plugin import CoreWarnings from buildstream.plugin import CoreWarnings
from buildstream.plugins.sources.git import GitMirror
from tests.testutils import cli, create_repo from tests.testutils import cli, create_repo
from tests.testutils.site import HAVE_GIT from tests.testutils.site import HAVE_GIT
...@@ -36,6 +39,64 @@ DATA_DIR = os.path.join( ...@@ -36,6 +39,64 @@ DATA_DIR = os.path.join(
) )
@pytest.mark.parametrize(
["type_", "setup"],
[
("inexistent-dir", lambda path: None),
("empty-dir", lambda path: path.mkdir()),
("non-empty-dir", lambda path: path.join("bad").write("hi", ensure=True)),
("file", lambda path: path.write("no")),
],
)
def test_git_mirror_atomic_move(tmpdir, type_, setup):
# Create required elements
class DummySource:
def get_mirror_directory(self):
return str(tmpdir.join("source").mkdir())
status = mock.MagicMock()
url = "file://{}/url".format(tmpdir)
# Create fake download directory
download_dir = tmpdir.join("download_dir")
download_dir.join("oracle").write("hello", ensure=True)
# Initiate mirror
mirror = GitMirror(DummySource(), None, url, None)
# Setup destination directory
setup(py.path.local(mirror.mirror))
# Copy directory content
if type_ == "file":
with pytest.raises(SourceError):
mirror._atomic_move_mirror(str(download_dir), mirror.url)
else:
mirror._atomic_move_mirror(str(download_dir), mirror.url)
# Validate
if type_ == "non-empty-dir":
# With a non empty directory, we should not have overriden
# and notified a status
assert DummySource.status.called
expected_oracle = os.path.join(mirror.mirror, "bad")
expected_content = "hi"
elif type_ == "file":
expected_oracle = mirror.mirror
expected_content = "no"
else:
# Otherwise we should have a new directory and the data
expected_oracle = os.path.join(mirror.mirror, "oracle")
expected_content = "hello"
assert os.path.exists(expected_oracle)
with open(expected_oracle) as fp:
assert fp.read() == expected_content
@pytest.mark.skipif(HAVE_GIT is False, reason="git is not available") @pytest.mark.skipif(HAVE_GIT is False, reason="git is not available")
@pytest.mark.datafiles(os.path.join(DATA_DIR, 'template')) @pytest.mark.datafiles(os.path.join(DATA_DIR, 'template'))
def test_fetch_bad_ref(cli, tmpdir, datafiles): def test_fetch_bad_ref(cli, tmpdir, datafiles):
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment