diff --git a/CHANGES b/CHANGES index 1c7283f2..b0903eee 100644 --- a/CHANGES +++ b/CHANGES @@ -15,11 +15,25 @@ $ pip install --user --upgrade --pre libvcs +### New features + +#### pytest plugin: Authorship fixtures (#476) + +- New, customizable session-scoped fixtures for default committer on Mercurial and Git: + - Name: {func}`libvcs.pytest_plugin.vcs_name` + - Email: {func}`libvcs.pytest_plugin.vcs_email` + - User (e.g. _`user `_): {func}`libvcs.pytest_plugin.vcs_user` + - For git only: {func}`libvcs.pytest_plugin.git_commit_envvars` + +#### pytest plugins: Default repos use authorship fixtures (#476) + +New repos will automatically apply these session-scoped fixtures. + ## libvcs 0.32.3 (2024-10-13) ### Bug fixes -- Pytest fixtures `hg_remote_repo_single_commit_post_init()` and `git_remote_repo_single_commit_post_init()` now support passing `env` for VCS configuration. +- Pytest fixtures `hg_remote_repo_single_commit_post_init()` and `git_remote_repo_single_commit_post_init()` now support passing `env` for VCS configuration. Both functions accept `hgconfig` and `gitconfig` fixtures, now applied in the `hg_repo` and `git_repo` fixtures. diff --git a/docs/pytest-plugin.md b/docs/pytest-plugin.md index 64e8e92e..22aeb435 100644 --- a/docs/pytest-plugin.md +++ b/docs/pytest-plugin.md @@ -37,7 +37,7 @@ This pytest plugin works by providing {ref}`pytest fixtures `_): {func}`vcs_user` + - For git only: {func}`git_commit_envvars` These ensure that repositories can be cloned and created without unnecessary warnings. @@ -74,10 +79,19 @@ def setup(set_home: None): pass ``` -### Setting a Default VCS Configuration +### VCS Configuration #### Git +You can override the default author used in {func}`git_remote_repo` and other +fixtures via {func}`vcs_name`, {func}`vcs_email`, and {func}`vcs_user`: + +``` +@pytest.fixture(scope="session") +def vcs_name() -> str: + return "My custom name" +``` + Use the {func}`set_gitconfig` fixture with `autouse=True`: ```python @@ -88,6 +102,27 @@ def setup(set_gitconfig: None): pass ``` +Sometimes, `set_getconfig` via `GIT_CONFIG` doesn't apply as expected. For those +cases, you can use {func}`git_commit_envvars`: + +```python +import pytest + +@pytest.fixture +def my_git_repo( + create_git_remote_repo: CreateRepoPytestFixtureFn, + gitconfig: pathlib.Path, + git_commit_envvars: "_ENV", +) -> pathlib.Path: + """Copy the session-scoped Git repository to a temporary directory.""" + repo_path = create_git_remote_repo() + git_remote_repo_single_commit_post_init( + remote_repo_path=repo_path, + env=git_commit_envvars, + ) + return repo_path +``` + #### Mercurial Use the {func}`set_hgconfig` fixture with `autouse=True`: diff --git a/src/libvcs/pytest_plugin.py b/src/libvcs/pytest_plugin.py index 0f46ffda..48f2bd7d 100644 --- a/src/libvcs/pytest_plugin.py +++ b/src/libvcs/pytest_plugin.py @@ -46,6 +46,43 @@ def __init__(self, attempts: int, *args: object) -> None: ) +DEFAULT_VCS_NAME = "Test user" +DEFAULT_VCS_EMAIL = "test@example.com" + + +@pytest.fixture(scope="session") +def vcs_name() -> str: + """Return default VCS name.""" + return DEFAULT_VCS_NAME + + +@pytest.fixture(scope="session") +def vcs_email() -> str: + """Return default VCS email.""" + return DEFAULT_VCS_EMAIL + + +@pytest.fixture(scope="session") +def vcs_user(vcs_name: str, vcs_email: str) -> str: + """Return default VCS user.""" + return f"{vcs_name} <{vcs_email}>" + + +@pytest.fixture(scope="session") +def git_commit_envvars(vcs_name: str, vcs_email: str) -> "_ENV": + """Return environment variables for `git commit`. + + For some reason, `GIT_CONFIG` via {func}`set_gitconfig` doesn't work for `git + commit`. + """ + return { + "GIT_AUTHOR_NAME": vcs_name, + "GIT_AUTHOR_EMAIL": vcs_email, + "GIT_COMMITTER_NAME": vcs_name, + "GIT_COMMITTER_EMAIL": vcs_email, + } + + class RandomStrSequence: """Create a random string sequence.""" @@ -110,13 +147,12 @@ def set_home( monkeypatch.setenv("HOME", str(user_path)) -vcs_email = "libvcs@git-pull.com" - - @pytest.fixture(scope="session") @skip_if_git_missing def gitconfig( user_path: pathlib.Path, + vcs_email: str, + vcs_name: str, ) -> pathlib.Path: """Return git configuration, pytest fixture.""" gitconfig = user_path / ".gitconfig" @@ -129,7 +165,7 @@ def gitconfig( f""" [user] email = {vcs_email} - name = {getpass.getuser()} + name = {vcs_name} [color] diff = auto """, @@ -155,6 +191,7 @@ def set_gitconfig( @skip_if_hg_missing def hgconfig( user_path: pathlib.Path, + vcs_user: str, ) -> pathlib.Path: """Return Mercurial configuration.""" hgrc = user_path / ".hgrc" @@ -162,7 +199,7 @@ def hgconfig( textwrap.dedent( f""" [ui] - username = libvcs tests + username = {vcs_user} merge = internal:merge [trusted] @@ -237,7 +274,11 @@ def unique_repo_name(remote_repos_path: pathlib.Path, max_retries: int = 15) -> class CreateRepoPostInitFn(Protocol): """Typing for VCS repo creation callback.""" - def __call__(self, remote_repo_path: pathlib.Path) -> None: + def __call__( + self, + remote_repo_path: pathlib.Path, + env: "_ENV | None" = None, + ) -> None: """Ran after creating a repo from pytest fixture.""" ... @@ -263,6 +304,7 @@ def _create_git_remote_repo( remote_repo_path: pathlib.Path, remote_repo_post_init: Optional[CreateRepoPostInitFn] = None, init_cmd_args: InitCmdArgs = DEFAULT_GIT_REMOTE_REPO_CMD_ARGS, + env: "_ENV | None" = None, ) -> pathlib.Path: if init_cmd_args is None: init_cmd_args = [] @@ -272,7 +314,7 @@ def _create_git_remote_repo( ) if remote_repo_post_init is not None and callable(remote_repo_post_init): - remote_repo_post_init(remote_repo_path=remote_repo_path) + remote_repo_post_init(remote_repo_path=remote_repo_path, env=env) return remote_repo_path @@ -402,10 +444,14 @@ def git_remote_repo_single_commit_post_init( run( ["touch", testfile_filename], cwd=remote_repo_path, - env={"GITCONFIG": str(gitconfig)}, + env=env, + ) + run(["git", "add", testfile_filename], cwd=remote_repo_path, env=env) + run( + ["git", "commit", "-m", "test file for dummyrepo"], + cwd=remote_repo_path, + env=env, ) - run(["git", "add", testfile_filename], cwd=remote_repo_path) - run(["git", "commit", "-m", "test file for dummyrepo"], cwd=remote_repo_path) @pytest.fixture(scope="session") @@ -413,15 +459,14 @@ def git_remote_repo_single_commit_post_init( def git_remote_repo( create_git_remote_repo: CreateRepoPytestFixtureFn, gitconfig: pathlib.Path, + git_commit_envvars: "_ENV", ) -> pathlib.Path: """Copy the session-scoped Git repository to a temporary directory.""" # TODO: Cache the effect of of this in a session-based repo repo_path = create_git_remote_repo() git_remote_repo_single_commit_post_init( remote_repo_path=repo_path, - env={ - "GITCONFIG": str(gitconfig), - }, + env=git_commit_envvars, ) return repo_path @@ -596,6 +641,7 @@ def empty_hg_repo( def create_hg_remote_repo( remote_repos_path: pathlib.Path, empty_hg_repo: pathlib.Path, + hgconfig: pathlib.Path, ) -> CreateRepoPytestFixtureFn: """Pre-made hg repo, bare, used as a file:// remote to checkout and commit to.""" @@ -612,7 +658,10 @@ def fn( shutil.copytree(empty_hg_repo, remote_repo_path) if remote_repo_post_init is not None and callable(remote_repo_post_init): - remote_repo_post_init(remote_repo_path=remote_repo_path) + remote_repo_post_init( + remote_repo_path=remote_repo_path, + env={"HGRCPATH": str(hgconfig)}, + ) assert empty_hg_repo.exists() @@ -633,7 +682,8 @@ def hg_remote_repo( """Pre-made, file-based repo for push and pull.""" repo_path = create_hg_remote_repo() hg_remote_repo_single_commit_post_init( - remote_repo_path=repo_path, env={"HGRCPATH": str(hgconfig)} + remote_repo_path=repo_path, + env={"HGRCPATH": str(hgconfig)}, ) return repo_path @@ -731,6 +781,8 @@ def add_doctest_fixtures( doctest_namespace: dict[str, Any], tmp_path: pathlib.Path, set_home: pathlib.Path, + git_commit_envvars: "_ENV", + hgconfig: pathlib.Path, create_git_remote_repo: CreateRepoPytestFixtureFn, create_svn_remote_repo: CreateRepoPytestFixtureFn, create_hg_remote_repo: CreateRepoPytestFixtureFn, @@ -745,7 +797,10 @@ def add_doctest_fixtures( if shutil.which("git"): doctest_namespace["create_git_remote_repo"] = functools.partial( create_git_remote_repo, - remote_repo_post_init=git_remote_repo_single_commit_post_init, + remote_repo_post_init=functools.partial( + git_remote_repo_single_commit_post_init, + env=git_commit_envvars, + ), init_cmd_args=None, ) doctest_namespace["create_git_remote_repo_bare"] = create_git_remote_repo @@ -760,5 +815,8 @@ def add_doctest_fixtures( doctest_namespace["create_hg_remote_repo_bare"] = create_hg_remote_repo doctest_namespace["create_hg_remote_repo"] = functools.partial( create_hg_remote_repo, - remote_repo_post_init=hg_remote_repo_single_commit_post_init, + remote_repo_post_init=functools.partial( + hg_remote_repo_single_commit_post_init, + env={"HGRCPATH": str(hgconfig)}, + ), ) diff --git a/tests/test_pytest_plugin.py b/tests/test_pytest_plugin.py index 072f677a..ee91d742 100644 --- a/tests/test_pytest_plugin.py +++ b/tests/test_pytest_plugin.py @@ -7,7 +7,7 @@ import pytest from libvcs._internal.run import run -from libvcs.pytest_plugin import CreateRepoPytestFixtureFn, vcs_email +from libvcs.pytest_plugin import CreateRepoPytestFixtureFn @pytest.mark.skipif(not shutil.which("git"), reason="git is not available") @@ -36,11 +36,34 @@ def test_create_svn_remote_repo( assert svn_remote_1 != svn_remote_2 -def test_plugin( +def test_gitconfig( + gitconfig: pathlib.Path, + set_gitconfig: pathlib.Path, + vcs_email: str, +) -> None: + """Test gitconfig fixture.""" + output = run(["git", "config", "--get", "user.email"]) + used_config_file_output = run( + [ + "git", + "config", + "--show-origin", + "--get", + "user.email", + ], + ) + assert str(gitconfig) in used_config_file_output + assert vcs_email in output, "Should use our fixture config and home directory" + + +def test_git_fixtures( pytester: pytest.Pytester, monkeypatch: pytest.MonkeyPatch, + tmp_path: pathlib.Path, ) -> None: - """Tests for libvcs pytest plugin at large.""" + """Tests for libvcs pytest plugin git configuration.""" + monkeypatch.setenv("HOME", str(tmp_path)) + # Initialize variables pytester.plugins = ["pytest_plugin"] pytester.makefile( @@ -58,6 +81,10 @@ def test_plugin( import pathlib import pytest +@pytest.fixture(scope="session") +def vcs_email() -> str: + return "custom_email@testemail.com" + @pytest.fixture(autouse=True) def setup( request: pytest.FixtureRequest, @@ -75,9 +102,13 @@ def setup( import pathlib from libvcs.sync.git import GitSync -from libvcs.pytest_plugin import CreateRepoPytestFixtureFn +from libvcs.pytest_plugin import ( + CreateRepoPytestFixtureFn, + git_remote_repo_single_commit_post_init +) -def test_repo_git_remote_checkout( + +def test_repo_git_remote_repo_and_sync( create_git_remote_repo: CreateRepoPytestFixtureFn, tmp_path: pathlib.Path, projects_path: pathlib.Path, @@ -93,6 +124,35 @@ def test_repo_git_remote_checkout( assert git_repo_checkout_dir.exists() assert pathlib.Path(git_repo_checkout_dir / ".git").exists() + + +def test_git_bare_repo_sync_and_commit( + create_git_remote_bare_repo: CreateRepoPytestFixtureFn, + projects_path: pathlib.Path, +) -> None: + git_server = create_git_remote_bare_repo() + git_repo_checkout_dir = projects_path / "my_git_checkout" + git_repo = GitSync(path=str(git_repo_checkout_dir), url=f"file://{git_server!s}") + + git_repo.obtain() + git_repo.update_repo() + + assert git_repo.get_revision() == "initial" + + assert git_repo_checkout_dir.exists() + assert pathlib.Path(git_repo_checkout_dir / ".git").exists() + + git_remote_repo_single_commit_post_init( + remote_repo_path=git_repo_checkout_dir + ) + + assert git_repo.get_revision() != "initial" + + last_committer_email = git_repo.cmd.run(["log", "-1", "--pretty=format:%ae"]) + + assert last_committer_email == "custom_email@testemail.com", ( + 'Email should use the override from the "vcs_email" fixture' + ) """, ), } @@ -109,23 +169,4 @@ def test_repo_git_remote_checkout( # Test result = pytester.runpytest(str(first_test_filename)) - result.assert_outcomes(passed=1) - - -def test_gitconfig( - gitconfig: pathlib.Path, - set_gitconfig: pathlib.Path, -) -> None: - """Test gitconfig fixture.""" - output = run(["git", "config", "--get", "user.email"]) - used_config_file_output = run( - [ - "git", - "config", - "--show-origin", - "--get", - "user.email", - ], - ) - assert str(gitconfig) in used_config_file_output - assert vcs_email in output, "Should use our fixture config and home directory" + result.assert_outcomes(passed=2)