Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helpers #35

Merged
merged 6 commits into from
Nov 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
'baregitrepo',
'annexrepo',
'gitrepo',
'modified_dataset',
'skip_when_symlinks_not_supported',
'symlinks_supported',
'verify_pristine_gitconfig_global',
]

Expand All @@ -21,6 +24,12 @@
cfgman,
# function-scope temporary Git repo
gitrepo,
# session-scope repository with complex unsafed modifications
modified_dataset,
# function-scope auto-skip when `symlinks_supported` is False
skip_when_symlinks_not_supported,
# session-scope flag if symlinks are supported in test directories
symlinks_supported,
# verify no test leave contaminated config behind
verify_pristine_gitconfig_global,
)
15 changes: 14 additions & 1 deletion datalad_core/repo/repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,20 @@ def init_annex(
*,
autoenable_remotes: bool = True,
) -> BareRepoAnnex:
""" """
"""Initialize an annex in the repository

This is done be calling ``git annex init``. If an annex already exists,
it will be reinitialized.

The ``description`` parameter can be used to label the annex.
Otherwise, git-annex will auto-generate a description based on
username, hostname and the path of the repository.

The boolean flag ``autoenable_remotes`` controls whether or not
git-annex should honor a special remote's configuration to get
auto-enable on initialization.
"""
# refuse for non-bare
if self.config.get('core.bare', False).value is False:
msg = (
'Cannot initialize annex in a non-bare repository, '
Expand Down
4 changes: 2 additions & 2 deletions datalad_core/repo/tests/test_repo.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from shutil import rmtree

import pytest

from datalad_core.tests import rmtree

from ..repo import Repo


Expand Down
11 changes: 10 additions & 1 deletion datalad_core/repo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ def init_annex_at(
description: str | None = None,
autoenable_remotes: bool = True,
) -> None:
"""Call ``git-annex init`` at a given ``path``"""
"""Call ``git-annex init`` at a given ``path``

The ``description`` parameter can be used to label the annex. Otherwise,
git-annex will auto-generate a description based on username, hostname, and
the path of the repository.

The boolean flag ``autoenable_remotes`` controls whether or not
git-annex should honor a special remote's configuration to get
auto-enabled on initialization.
"""
cmd = ['init']
if not autoenable_remotes:
# no, we do not set --autoenable, this is a RepoAnnex feature
Expand Down
15 changes: 13 additions & 2 deletions datalad_core/repo/worktree.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,19 @@ def init_annex(
*,
autoenable_remotes: bool = True,
) -> Annex:
""" """
# refuse for non-bare
"""Initialize an annex in the repository associated with the worktree

This is done be calling ``git annex init``. If an annex already exists,
it will be reinitialized.

The ``description`` parameter can be used to label the annex.
Otherwise, git-annex will auto-generate a description based on
username, hostname and the path of the repository.

The boolean flag ``autoenable_remotes`` controls whether or not
git-annex should honor a special remote's configuration to get
auto-enable on initialization.
"""
init_annex_at(
self.path,
description=description,
Expand Down
20 changes: 15 additions & 5 deletions datalad_core/runners/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,9 @@ def call_git(
*,
cwd: Path | None = None,
force_c_locale: bool = False,
text: bool | None = None,
capture_output: bool = False,
) -> None:
) -> str | bytes | None:
"""Call Git with no output capture, raises on non-zero exit.

If ``cwd`` is not None, the function changes the working directory to
Expand All @@ -87,17 +88,26 @@ def call_git(
is altered to ensure output according to the C locale. This is useful
when output has to be processed in a locale invariant fashion.

If ``capture_output`` is ``True``, process output is captured. This is
necessary for reporting any error messaging via a ``CommandError`` exception.
By default process output is not captured.
If ``capture_output`` is ``True``, process output is captured (and not
relayed to the parent process/terminal). This is necessary for reporting
any error messaging via a ``CommandError`` exception. By default process
output is not captured.

All other argument are pass on to ``subprocess.run()`` verbatim.

If ``capture_output`` is enabled, the captured STDOUT is returned as
``str`` or ``bytes``, depending on the value of ``text``. Otherwise
``None`` is returned to indicate that no output was captured.
"""
_call_git(
res = _call_git(
args,
capture_output=capture_output,
cwd=cwd,
check=True,
text=text,
force_c_locale=force_c_locale,
)
return res.stdout if capture_output else None


def call_git_success(
Expand Down
7 changes: 5 additions & 2 deletions datalad_core/runners/tests/test_callgit.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@


def test_call_git():
# smoke test
call_git(['--version'])
# by default no output is captured
assert call_git(['--version']) is None
# capture gives bytes unless text=True
assert b'version' in call_git(['--version'], capture_output=True)
assert 'version' in call_git(['--version'], text=True, capture_output=True)
# raises properly
with pytest.raises(CommandError):
call_git(['notacommand'])
Expand Down
9 changes: 9 additions & 0 deletions datalad_core/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__all__ = [
'call_git_addcommit',
'rmtree',
]

from .utils import (
call_git_addcommit,
rmtree,
)
108 changes: 107 additions & 1 deletion datalad_core/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
import pytest

from datalad_core.config import get_manager
from datalad_core.runners import call_git
from datalad_core.runners import (
call_git,
call_git_lines,
)
from datalad_core.tests.utils import modify_dataset

magic_marker = 'c4d0de12-8008-11ef-86ea-3776083add61'
standard_gitconfig = f"""\
Expand Down Expand Up @@ -140,3 +144,105 @@ def annexrepo(gitrepo) -> Generator[Path]:
capture_output=True,
)
return gitrepo


@pytest.fixture(autouse=False, scope='session')
def symlinks_supported(tmp_path_factory) -> bool:
"""Returns whether creating symlinks is supported in test directories"""
testdir = tmp_path_factory.mktemp('symlink_check')
target = testdir / 'target'
source = testdir / 'source'
try:
target.touch()
source.symlink_to(target)
except Exception: # noqa: BLE001
return False
return True


@pytest.fixture(autouse=False, scope='function') # noqa: PT003
def skip_when_symlinks_not_supported(symlinks_supported):
if not symlinks_supported:
msg = 'skipped, symlinks are not supported in the test directory'
raise pytest.skip(msg)


@pytest.fixture(scope='session')
def modified_dataset(tmp_path_factory):
"""Produces a dataset with various modifications

The fixture is module-scope, aiming to be reused by many tests focused
on reporting. It does not support any further modification. The fixture
will fail, if any such modification is detected. Use the helper
``modify_dataset()`` to apply these modification to an existing repository
to circumwent these restriction.

``git status`` will report::

> git status -uall
On branch dl-test-branch
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: dir_m/file_a
new file: file_a
new file: file_am

Changes not staged for commit:
(use "git add/rm <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
(commit or discard the untracked or modified content in submodules)
deleted: dir_d/file_d
deleted: dir_m/file_d
modified: dir_m/file_m
deleted: dir_sm/sm_d
modified: dir_sm/sm_m (modified content)
modified: dir_sm/sm_mu (modified content, untracked content)
modified: dir_sm/sm_n (new commits)
modified: dir_sm/sm_nm (new commits, modified content)
modified: dir_sm/sm_nmu (new commits, modified content, untracked content)
modified: dir_sm/sm_u (untracked content)
modified: file_am
deleted: file_d
modified: file_m

Untracked files:
(use "git add <file>..." to include in what will be committed)
dir_m/dir_u/file_u
dir_m/file_u
dir_u/file_u
file_u


Suffix indicates the ought-to state (multiple possible):

a - added
c - clean
d - deleted
n - new commits
m - modified
u - untracked content

Prefix indicated the item type:

file - file
sm - submodule
dir - directory
"""
path = tmp_path_factory.mktemp('gitrepo')
call_git(
['init'],
cwd=path,
capture_output=True,
)
# the returned status promise is the comparison reference
status_promise = modify_dataset(path).splitlines()

yield path
# compare with initial git-status output, if there are any
# differences the assumptions of any consuming test could be
# invalidated. The modifying code must be found and fixed
if status_promise != call_git_lines(
['status', '-uall', '--porcelain=v1'], cwd=path
):
msg = 'Unexpected modification of the testbed'
raise AssertionError(msg)
6 changes: 6 additions & 0 deletions datalad_core/tests/test_fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def test_modified_dataset_fixture(modified_dataset):
assert modified_dataset
# the test is to do nothing.
# the dataset modification will be done, the promised capture
# before the test starts. the test will then do nothing, and
# the promise is reevaluated at the end and must succeed.
12 changes: 12 additions & 0 deletions datalad_core/tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from datalad_core.runners import call_git
from datalad_core.tests.utils import modify_dataset


def test_modify_dataset(gitrepo):
promise = modify_dataset(gitrepo)
assert promise == call_git(
['status', '-uall', '--porcelain=v1'],
cwd=gitrepo,
capture_output=True,
text=True,
)
Loading