Skip to content

Commit

Permalink
feat: test helper modify_dataset() and modified_dataset
Browse files Browse the repository at this point in the history
This is a more flexible and more robust variant of the datalad-next
fixture `modified_dataset`, split into a test helper and the actual
fixture.

Unlike the datalad-next version, it does not require any datalad
commands to work, just plain Git.
  • Loading branch information
mih committed Nov 1, 2024
1 parent b586815 commit 9b2bb1f
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 4 deletions.
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
'baregitrepo',
'annexrepo',
'gitrepo',
'modified_dataset',
'skip_when_symlinks_not_supported',
'symlinks_supported',
'verify_pristine_gitconfig_global',
Expand All @@ -23,6 +24,8 @@
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
Expand Down
87 changes: 86 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 @@ -161,3 +165,84 @@ 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,
)
135 changes: 132 additions & 3 deletions datalad_core/tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
from pathlib import (
Path,
)
from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from pathlib import (
Path,
PurePath,
)

from shutil import rmtree

from datalad_core.repo import Worktree
from datalad_core.runners import (
call_git,
)
Expand All @@ -20,3 +30,122 @@ def call_git_addcommit(

call_git(['add'] + [str(p) for p in paths], cwd=cwd, capture_output=True)
call_git(['commit', '-m', msg], cwd=cwd, capture_output=True)


# Target `git status -uall --porcelain=v1` of `modify_dataset()` result
modify_dataset_promise = """\
D dir_d/file_d
A dir_m/file_a
D dir_m/file_d
M dir_m/file_m
D dir_sm/sm_d
M dir_sm/sm_m
M dir_sm/sm_mu
M dir_sm/sm_n
M dir_sm/sm_nm
M dir_sm/sm_nmu
M dir_sm/sm_u
A file_a
AM file_am
D file_d
M file_m
?? dir_m/dir_u/file_u
?? dir_m/file_u
?? dir_u/file_u
?? file_u
"""


def modify_dataset(path: Path) -> str:
"""Applies the modification for the ``modified_dataset`` fixture
``path`` is a directory in an existing Git repository.
This is provided as a separate function for the case where the
modification themselves need to be modified. The fixture checks
that this does not happen.
The function returns the ``git status -uall --porcelain=v1``
report that it aimed to create. This is not a status report
queried after this function ran, but a "promise" that can be used
to inspect the state of a repository.
"""
ds_dir = path / 'dir_m'
ds_dir_d = path / 'dir_d'
dirsm = path / 'dir_sm'
for d in (ds_dir, ds_dir_d, dirsm):
d.mkdir()
(ds_dir / 'file_m').touch()
(path / 'file_m').touch()
dss: dict[str, Path] = {}
smnames = (
'sm_d',
'sm_c',
'sm_n',
'sm_m',
'sm_nm',
'sm_u',
'sm_mu',
'sm_nmu',
'droppedsm_c',
)
for smname in smnames:
sds_path = dirsm / smname
sds_path.mkdir()
sds = Worktree.init_at(sds_path)
# we need some content for a commit
(sds_path / '.gitkeep').touch()
# for the plain modification, commit the reference right here
if smname in ('sm_m', 'sm_nm', 'sm_mu', 'sm_nmu'):
(sds_path / 'file_m').touch()
call_git_addcommit(sds_path)
dss[smname] = sds.path
call_git(
['submodule', 'add', f'./{sds_path}', f'{sds_path.relative_to(path)}'],
cwd=path,
capture_output=True,
)
# files in superdataset to be deleted
for d in (ds_dir_d, ds_dir, path):
(d / 'file_d').touch()
dss['.'] = path
dss['dir'] = ds_dir
call_git_addcommit(path)
call_git(
['submodule', 'deinit', '--force', str(dirsm / 'droppedsm_c')],
cwd=path,
capture_output=True,
)
# a new commit
for smname in ('.', 'sm_n', 'sm_nm', 'sm_nmu'):
sub = dss[smname]
(sub / 'file_c').touch()
call_git_addcommit(sub)
# modified file
for smname in ('.', 'dir', 'sm_m', 'sm_nm', 'sm_mu', 'sm_nmu'):
pobj = dss[smname]
(pobj / 'file_m').write_text('modify!')
# untracked
for smname in ('.', 'dir', 'sm_u', 'sm_mu', 'sm_nmu'):
pobj = dss[smname]
(pobj / 'file_u').touch()
(pobj / 'dirempty_u').mkdir()
(pobj / 'dir_u').mkdir()
(pobj / 'dir_u' / 'file_u').touch()
# delete items
rmtree(dss['sm_d'])
rmtree(ds_dir_d)
(ds_dir / 'file_d').unlink()
(path / 'file_d').unlink()
# added items
for smname in ('.', 'dir', 'sm_m', 'sm_nm', 'sm_mu', 'sm_nmu'):
pobj = dss[smname]
(pobj / 'file_a').write_text('added')
call_git(['add', 'file_a'], cwd=pobj, capture_output=True)
# added and then modified file
file_am_obj = path / 'file_am'
file_am_obj.write_text('added')
call_git(['add', 'file_am'], cwd=path, capture_output=True)
file_am_obj.write_text('modified')

return modify_dataset_promise

0 comments on commit 9b2bb1f

Please sign in to comment.