diff --git a/.github/workflows/cygwin-test.yml b/.github/workflows/cygwin-test.yml index 96728e51a..13a01dec4 100644 --- a/.github/workflows/cygwin-test.yml +++ b/.github/workflows/cygwin-test.yml @@ -70,9 +70,7 @@ jobs: command -v git python git version python --version - python -c 'import sys; print(sys.platform)' - python -c 'import os; print(os.name)' - python -c 'import git; print(git.compat.is_win)' # NOTE: Deprecated. Use os.name directly. + python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' - name: Test with pytest run: | diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 35fecf16c..3915296ef 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -10,18 +10,19 @@ permissions: jobs: build: - runs-on: ubuntu-latest - strategy: fail-fast: false matrix: + os: ["ubuntu-latest", "windows-latest"] python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - experimental: false + runs-on: ${{ matrix.os }} + defaults: run: - shell: /bin/bash --noprofile --norc -exo pipefail {0} + shell: bash --noprofile --norc -exo pipefail {0} steps: - uses: actions/checkout@v4 @@ -34,6 +35,12 @@ jobs: python-version: ${{ matrix.python-version }} allow-prereleases: ${{ matrix.experimental }} + - name: Set up WSL (Windows) + if: startsWith(matrix.os, 'windows') + uses: Vampire/setup-wsl@v2.0.2 + with: + distribution: Debian + - name: Prepare this repo for tests run: | ./init-tests-after-clone.sh @@ -61,9 +68,16 @@ jobs: command -v git python git version python --version - python -c 'import sys; print(sys.platform)' - python -c 'import os; print(os.name)' - python -c 'import git; print(git.compat.is_win)' # NOTE: Deprecated. Use os.name directly. + python -c 'import os, sys; print(f"sys.platform={sys.platform!r}, os.name={os.name!r}")' + + # For debugging hook tests on native Windows systems that may have WSL. + - name: Show bash.exe candidates (Windows) + if: startsWith(matrix.os, 'windows') + run: | + set +e + bash.exe -c 'printenv WSL_DISTRO_NAME; uname -a' + python -c 'import subprocess; subprocess.run(["bash.exe", "-c", "printenv WSL_DISTRO_NAME; uname -a"])' + continue-on-error: true - name: Check types with mypy run: | diff --git a/test-requirements.txt b/test-requirements.txt index 7cfb977a1..fcdc93c1d 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,3 +9,4 @@ pytest-cov pytest-instafail pytest-mock pytest-sugar +sumtypes diff --git a/test/test_config.py b/test/test_config.py index 0e1bba08a..c1b26c91f 100644 --- a/test/test_config.py +++ b/test/test_config.py @@ -6,19 +6,15 @@ import glob import io import os +import os.path as osp from unittest import mock +import pytest + from git import GitConfigParser from git.config import _OMD, cp -from test.lib import ( - TestCase, - fixture_path, - SkipTest, -) -from test.lib import with_rw_directory - -import os.path as osp from git.util import rmfile +from test.lib import SkipTest, TestCase, fixture_path, with_rw_directory _tc_lock_fpaths = osp.join(osp.dirname(__file__), "fixtures/*.lock") @@ -239,6 +235,11 @@ def check_test_value(cr, value): with GitConfigParser(fpa, read_only=True) as cr: check_test_value(cr, tv) + @pytest.mark.xfail( + os.name == "nt", + reason='Second config._has_includes() assertion fails (for "config is included if path is matching git_dir")', + raises=AssertionError, + ) @with_rw_directory def test_conditional_includes_from_git_dir(self, rw_dir): # Initiate repository path. diff --git a/test/test_diff.py b/test/test_diff.py index 1678e737d..1d138a086 100644 --- a/test/test_diff.py +++ b/test/test_diff.py @@ -3,26 +3,17 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ -import ddt +import os +import os.path as osp import shutil import tempfile -from git import ( - Repo, - GitCommandError, - Diff, - DiffIndex, - NULL_TREE, - Submodule, -) -from git.cmd import Git -from test.lib import ( - TestBase, - StringProcessAdapter, - fixture, -) -from test.lib import with_rw_directory -import os.path as osp +import ddt +import pytest + +from git import NULL_TREE, Diff, DiffIndex, GitCommandError, Repo, Submodule +from git.cmd import Git +from test.lib import StringProcessAdapter, TestBase, fixture, with_rw_directory def to_raw(input): @@ -318,6 +309,11 @@ def test_diff_with_spaces(self): self.assertIsNone(diff_index[0].a_path, repr(diff_index[0].a_path)) self.assertEqual(diff_index[0].b_path, "file with spaces", repr(diff_index[0].b_path)) + @pytest.mark.xfail( + os.name == "nt", + reason='"Access is denied" when tearDown calls shutil.rmtree', + raises=PermissionError, + ) def test_diff_submodule(self): """Test that diff is able to correctly diff commits that cover submodule changes""" # Init a temp git repo that will be referenced as a submodule. diff --git a/test/test_docs.py b/test/test_docs.py index 2f4b2e8d8..2ff1c794a 100644 --- a/test/test_docs.py +++ b/test/test_docs.py @@ -8,11 +8,10 @@ import pytest +from git.exc import GitCommandError from test.lib import TestBase from test.lib.helper import with_rw_directory -import os.path - class Tutorials(TestBase): def tearDown(self): @@ -207,6 +206,14 @@ def update(self, op_code, cur_count, max_count=None, message=""): assert sm.module_exists() # The submodule's working tree was checked out by update. # ![14-test_init_repo_object] + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_directory def test_references_and_objects(self, rw_dir): # [1-test_references_and_objects] diff --git a/test/test_fun.py b/test/test_fun.py index 566bc9aae..8ea5b7e46 100644 --- a/test/test_fun.py +++ b/test/test_fun.py @@ -3,10 +3,13 @@ from io import BytesIO from stat import S_IFDIR, S_IFREG, S_IFLNK, S_IXUSR -from os import stat +import os import os.path as osp +import pytest + from git import Git +from git.exc import GitCommandError from git.index import IndexFile from git.index.fun import ( aggressive_tree_merge, @@ -34,6 +37,14 @@ def _assert_index_entries(self, entries, trees): assert (entry.path, entry.stage) in index.entries # END assert entry matches fully + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) def test_aggressive_tree_merge(self): # Head tree with additions, removals and modification compared to its predecessor. odb = self.rorepo.odb @@ -291,12 +302,12 @@ def test_linked_worktree_traversal(self, rw_dir): rw_master.git.worktree("add", worktree_path, branch.name) dotgit = osp.join(worktree_path, ".git") - statbuf = stat(dotgit) + statbuf = os.stat(dotgit) self.assertTrue(statbuf.st_mode & S_IFREG) gitdir = find_worktree_git_dir(dotgit) self.assertIsNotNone(gitdir) - statbuf = stat(gitdir) + statbuf = os.stat(gitdir) self.assertTrue(statbuf.st_mode & S_IFDIR) def test_tree_entries_from_data_with_failing_name_decode_py3(self): diff --git a/test/test_index.py b/test/test_index.py index 3d1042be8..cd1c37efc 100644 --- a/test/test_index.py +++ b/test/test_index.py @@ -4,14 +4,17 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ from io import BytesIO +import logging import os import os.path as osp from pathlib import Path +import re from stat import S_ISLNK, ST_MODE -import shutil +import subprocess import tempfile import pytest +from sumtypes import constructor, sumtype from git import ( IndexFile, @@ -34,19 +37,121 @@ HOOKS_SHEBANG = "#!/usr/bin/env sh\n" +log = logging.getLogger(__name__) -def _found_in(cmd, directory): - """Check if a command is resolved in a directory (without following symlinks).""" - path = shutil.which(cmd) - return path and Path(path).parent == Path(directory) +def _get_windows_ansi_encoding(): + """Get the encoding specified by the Windows system-wide ANSI active code page.""" + # locale.getencoding may work but is only in Python 3.11+. Use the registry instead. + import winreg -is_win_without_bash = os.name == "nt" and not shutil.which("bash.exe") + hklm_path = R"SYSTEM\CurrentControlSet\Control\Nls\CodePage" + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, hklm_path) as key: + value, _ = winreg.QueryValueEx(key, "ACP") + return f"cp{value}" -is_win_with_wsl_bash = os.name == "nt" and _found_in( - cmd="bash.exe", - directory=Path(os.getenv("WINDIR")) / "System32", -) + +@sumtype +class WinBashStatus: + """Status of bash.exe for native Windows. Affects which commit hook tests can pass. + + Call check() to check the status. (CheckError and WinError should not typically be + used to trigger skip or xfail, because they represent unexpected situations.) + """ + + Inapplicable = constructor() + """This system is not native Windows: either not Windows at all, or Cygwin.""" + + Absent = constructor() + """No command for bash.exe is found on the system.""" + + Native = constructor() + """Running bash.exe operates outside any WSL distribution (as with Git Bash).""" + + Wsl = constructor() + """Running bash.exe calls bash in a WSL distribution.""" + + WslNoDistro = constructor("process", "message") + """Running bash.exe tries to run bash on a WSL distribution, but none exists.""" + + CheckError = constructor("process", "message") + """Running bash.exe fails in an unexpected error or gives unexpected output.""" + + WinError = constructor("exception") + """bash.exe may exist but can't run. CreateProcessW fails unexpectedly.""" + + @classmethod + def check(cls): + """Check the status of the bash.exe that run_commit_hook will try to use. + + This runs a command with bash.exe and checks the result. On Windows, shell and + non-shell executable search differ; shutil.which often finds the wrong bash.exe. + + run_commit_hook uses Popen, including to run bash.exe on Windows. It doesn't + pass shell=True (and shouldn't). On Windows, Popen calls CreateProcessW, which + checks some locations before using the PATH environment variable. It is expected + to try System32, even if another directory with the executable precedes it in + PATH. When WSL is present, even with no distributions, bash.exe usually exists + in System32; Popen finds it even if a shell would run another one, as on CI. + (Without WSL, System32 may still have bash.exe; users sometimes put it there.) + """ + if os.name != "nt": + return cls.Inapplicable() + + try: + # Output rather than forwarding the test command's exit status so that if a + # failure occurs before we even get to this point, we will detect it. For + # information on ways to check for WSL, see https://superuser.com/a/1749811. + script = 'test -e /proc/sys/fs/binfmt_misc/WSLInterop; echo "$?"' + command = ["bash.exe", "-c", script] + process = subprocess.run(command, capture_output=True) + except FileNotFoundError: + return cls.Absent() + except OSError as error: + return cls.WinError(error) + + text = cls._decode(process.stdout).rstrip() # stdout includes WSL's own errors. + + if process.returncode == 1 and re.search(r"\bhttps://aka.ms/wslstore\b", text): + return cls.WslNoDistro(process, text) + if process.returncode != 0: + log.error("Error running bash.exe to check WSL status: %s", text) + return cls.CheckError(process, text) + if text == "0": + return cls.Wsl() + if text == "1": + return cls.Native() + log.error("Strange output checking WSL status: %s", text) + return cls.CheckError(process, text) + + @staticmethod + def _decode(stdout): + """Decode bash.exe output as best we can.""" + # When bash.exe is the WSL wrapper but the output is from WSL itself rather than + # code running in a distribution, the output is often in UTF-16LE, which Windows + # uses internally. The UTF-16LE representation of a Windows-style line ending is + # rarely seen otherwise, so use it to detect this situation. + if b"\r\0\n\0" in stdout: + return stdout.decode("utf-16le") + + # At this point, the output is either blank or probably not UTF-16LE. It's often + # UTF-8 from inside a WSL distro or non-WSL bash shell. Our test command only + # uses the ASCII subset, so we can safely guess a wrong code page for it. Errors + # from such an environment can contain any text, but unlike WSL's own messages, + # they go to stderr, not stdout. So we can try the system ANSI code page first. + acp = _get_windows_ansi_encoding() + try: + return stdout.decode(acp) + except UnicodeDecodeError: + pass + except LookupError as error: + log.warning("%s", str(error)) # Message already says "Unknown encoding:". + + # Assume UTF-8. If invalid, substitute Unicode replacement characters. + return stdout.decode("utf-8", errors="replace") + + +_win_bash_status = WinBashStatus.check() def _make_hook(git_dir, name, content, make_exec=True): @@ -179,6 +284,14 @@ def add_bad_blob(): except Exception as ex: assert "index.lock' could not be obtained" not in str(ex) + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("0.1.6") def test_index_file_from_tree(self, rw_repo): common_ancestor_sha = "5117c9c8a4d3af19a9958677e45cda9269de1541" @@ -229,6 +342,14 @@ def test_index_file_from_tree(self, rw_repo): # END for each blob self.assertEqual(num_blobs, len(three_way_index.entries)) + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("0.1.6") def test_index_merge_tree(self, rw_repo): # A bit out of place, but we need a different repo for this: @@ -291,6 +412,14 @@ def test_index_merge_tree(self, rw_repo): self.assertEqual(len(unmerged_blobs), 1) self.assertEqual(list(unmerged_blobs.keys())[0], manifest_key[0]) + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("0.1.6") def test_index_file_diffing(self, rw_repo): # Default Index instance points to our index. @@ -425,6 +554,14 @@ def _count_existing(self, repo, files): # END num existing helper + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("0.1.6") def test_index_mutation(self, rw_repo): index = rw_repo.index @@ -778,6 +915,14 @@ def make_paths(): for absfile in absfiles: assert osp.isfile(absfile) + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("HEAD") def test_compare_write_tree(self, rw_repo): """Test writing all trees, comparing them for equality.""" @@ -877,12 +1022,22 @@ class Mocked: rel = index._to_relative_path(path) self.assertEqual(rel, os.path.relpath(path, root)) + @pytest.mark.xfail( + type(_win_bash_status) is WinBashStatus.WslNoDistro, + reason="Currently uses the bash.exe for WSL even with no WSL distro installed", + raises=HookExecutionError, + ) @with_rw_repo("HEAD", bare=True) def test_pre_commit_hook_success(self, rw_repo): index = rw_repo.index _make_hook(index.repo.git_dir, "pre-commit", "exit 0") index.commit("This should not fail") + @pytest.mark.xfail( + type(_win_bash_status) is WinBashStatus.WslNoDistro, + reason="Currently uses the bash.exe for WSL even with no WSL distro installed", + raises=AssertionError, + ) @with_rw_repo("HEAD", bare=True) def test_pre_commit_hook_fail(self, rw_repo): index = rw_repo.index @@ -890,7 +1045,7 @@ def test_pre_commit_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win_without_bash: + if type(_win_bash_status) is WinBashStatus.Absent: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, "") @@ -906,10 +1061,15 @@ def test_pre_commit_hook_fail(self, rw_repo): raise AssertionError("Should have caught a HookExecutionError") @pytest.mark.xfail( - is_win_without_bash or is_win_with_wsl_bash, + type(_win_bash_status) in {WinBashStatus.Absent, WinBashStatus.Wsl}, reason="Specifically seems to fail on WSL bash (in spite of #1399)", raises=AssertionError, ) + @pytest.mark.xfail( + type(_win_bash_status) is WinBashStatus.WslNoDistro, + reason="Currently uses the bash.exe for WSL even with no WSL distro installed", + raises=HookExecutionError, + ) @with_rw_repo("HEAD", bare=True) def test_commit_msg_hook_success(self, rw_repo): commit_message = "commit default head by Frèderic Çaufl€" @@ -923,6 +1083,11 @@ def test_commit_msg_hook_success(self, rw_repo): new_commit = index.commit(commit_message) self.assertEqual(new_commit.message, "{} {}".format(commit_message, from_hook_message)) + @pytest.mark.xfail( + type(_win_bash_status) is WinBashStatus.WslNoDistro, + reason="Currently uses the bash.exe for WSL even with no WSL distro installed", + raises=AssertionError, + ) @with_rw_repo("HEAD", bare=True) def test_commit_msg_hook_fail(self, rw_repo): index = rw_repo.index @@ -930,7 +1095,7 @@ def test_commit_msg_hook_fail(self, rw_repo): try: index.commit("This should fail") except HookExecutionError as err: - if is_win_without_bash: + if type(_win_bash_status) is WinBashStatus.Absent: self.assertIsInstance(err.status, OSError) self.assertEqual(err.command, [hp]) self.assertEqual(err.stdout, "") diff --git a/test/test_refs.py b/test/test_refs.py index 6ee385007..a1573c11b 100644 --- a/test/test_refs.py +++ b/test/test_refs.py @@ -4,8 +4,11 @@ # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ from itertools import chain +import os from pathlib import Path +import pytest + from git import ( Reference, Head, @@ -215,6 +218,14 @@ def test_head_checkout_detached_head(self, rw_repo): assert isinstance(res, SymbolicReference) assert res.name == "HEAD" + @pytest.mark.xfail( + os.name == "nt", + reason=( + "IndexFile.from_tree is broken on Windows (related to NamedTemporaryFile), see #1630.\n" + "'git read-tree --index-output=...' fails with 'fatal: unable to write new index file'." + ), + raises=GitCommandError, + ) @with_rw_repo("0.1.6") def test_head_reset(self, rw_repo): cur_head = rw_repo.head diff --git a/test/test_remote.py b/test/test_remote.py index 12de18476..080718a1c 100644 --- a/test/test_remote.py +++ b/test/test_remote.py @@ -3,36 +3,38 @@ # This module is part of GitPython and is released under the # 3-Clause BSD License: https://opensource.org/license/bsd-3-clause/ +import os +import os.path as osp +from pathlib import Path import random import tempfile -import pytest from unittest import skipIf +import pytest + from git import ( - RemoteProgress, + Commit, FetchInfo, - Reference, - SymbolicReference, + GitCommandError, Head, - Commit, PushInfo, + Reference, + Remote, + RemoteProgress, RemoteReference, + SymbolicReference, TagReference, - Remote, - GitCommandError, ) from git.cmd import Git -from pathlib import Path from git.exc import UnsafeOptionError, UnsafeProtocolError +from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList from test.lib import ( + GIT_DAEMON_PORT, TestBase, - with_rw_repo, - with_rw_and_rw_remote_repo, fixture, - GIT_DAEMON_PORT, + with_rw_and_rw_remote_repo, + with_rw_repo, ) -from git.util import rmtree, HIDE_WINDOWS_FREEZE_ERRORS, IterableList -import os.path as osp # Make sure we have repeatable results. @@ -766,6 +768,11 @@ def test_create_remote_unsafe_url(self, rw_repo): Remote.create(rw_repo, "origin", url) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=R"Multiple '\' instead of '/' in remote.url make it differ from expected value", + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_create_remote_unsafe_url_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -824,6 +831,15 @@ def test_fetch_unsafe_options(self, rw_repo): remote.fetch(**unsafe_option) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=( + "File not created. A separate Windows command may be needed. This and the " + "currently passing test test_fetch_unsafe_options must be adjusted in the " + "same way. Until then, test_fetch_unsafe_options is unreliable on Windows." + ), + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_fetch_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -883,6 +899,15 @@ def test_pull_unsafe_options(self, rw_repo): remote.pull(**unsafe_option) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=( + "File not created. A separate Windows command may be needed. This and the " + "currently passing test test_pull_unsafe_options must be adjusted in the " + "same way. Until then, test_pull_unsafe_options is unreliable on Windows." + ), + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_pull_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -948,6 +973,15 @@ def test_push_unsafe_options(self, rw_repo): remote.push(**unsafe_option) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=( + "File not created. A separate Windows command may be needed. This and the " + "currently passing test test_push_unsafe_options must be adjusted in the " + "same way. Until then, test_push_unsafe_options is unreliable on Windows." + ), + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_push_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: diff --git a/test/test_repo.py b/test/test_repo.py index e39aee05e..033deca1e 100644 --- a/test/test_repo.py +++ b/test/test_repo.py @@ -8,6 +8,7 @@ from io import BytesIO import itertools import os +import os.path as osp import pathlib import pickle import sys @@ -17,22 +18,22 @@ import pytest from git import ( + BadName, + Commit, + Git, + GitCmdObjectDB, + GitCommandError, + GitDB, + Head, + IndexFile, InvalidGitRepositoryError, - Repo, NoSuchPathError, - Head, - Commit, Object, - Tree, - IndexFile, - Git, Reference, - GitDB, - Submodule, - GitCmdObjectDB, Remote, - BadName, - GitCommandError, + Repo, + Submodule, + Tree, ) from git.exc import ( BadObject, @@ -43,8 +44,6 @@ from git.util import bin_to_hex, cygpath, join_path_native, rmfile, rmtree from test.lib import TestBase, fixture, with_rw_directory, with_rw_repo -import os.path as osp - def iter_flatten(lol): for items in lol: @@ -295,6 +294,15 @@ def test_clone_unsafe_options(self, rw_repo): rw_repo.clone(tmp_dir, **unsafe_option) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=( + "File not created. A separate Windows command may be needed. This and the " + "currently passing test test_clone_unsafe_options must be adjusted in the " + "same way. Until then, test_clone_unsafe_options is unreliable on Windows." + ), + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_clone_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -365,6 +373,15 @@ def test_clone_from_unsafe_options(self, rw_repo): Repo.clone_from(rw_repo.working_dir, tmp_dir, **unsafe_option) assert not tmp_file.exists() + @pytest.mark.xfail( + os.name == "nt", + reason=( + "File not created. A separate Windows command may be needed. This and the " + "currently passing test test_clone_from_unsafe_options must be adjusted in the " + "same way. Until then, test_clone_from_unsafe_options is unreliable on Windows." + ), + raises=AssertionError, + ) @with_rw_repo("HEAD") def test_clone_from_unsafe_options_allowed(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: @@ -1365,6 +1382,11 @@ def test_do_not_strip_newline_in_stdout(self, rw_dir): r.git.commit(message="init") self.assertEqual(r.git.show("HEAD:hello.txt", strip_newline_in_stdout=False), "hello\n") + @pytest.mark.xfail( + os.name == "nt", + reason=R"fatal: could not create leading directories of '--upload-pack=touch C:\Users\ek\AppData\Local\Temp\tmpnantqizc\pwn': Invalid argument", # noqa: E501 + raises=GitCommandError, + ) @with_rw_repo("HEAD") def test_clone_command_injection(self, rw_repo): with tempfile.TemporaryDirectory() as tdir: diff --git a/test/test_submodule.py b/test/test_submodule.py index 5be1a1077..7fbccf831 100644 --- a/test/test_submodule.py +++ b/test/test_submodule.py @@ -949,6 +949,18 @@ def test_remove_norefs(self, rwdir): sm.remove() assert not sm.exists() + @pytest.mark.xfail( + os.name == "nt" and sys.version_info >= (3, 12), + reason=( + "The sm.move call fails. Submodule.move calls os.renames, which raises:\n" + "PermissionError: [WinError 32] " + "The process cannot access the file because it is being used by another process: " + R"'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\mymodules\myname' " + R"-> 'C:\Users\ek\AppData\Local\Temp\test_renamekkbznwjp\parent\renamed\myname'" + "\nThis resembles other Windows errors, but only occurs starting in Python 3.12." + ), + raises=PermissionError, + ) @with_rw_directory def test_rename(self, rwdir): parent = git.Repo.init(osp.join(rwdir, "parent"))