Jelajahi Sumber

Improve test coverage and fix tag_create timezone bug

Jelmer Vernooij 1 Minggu lalu
induk
melakukan
caa06836ab

+ 1 - 1
dulwich/porcelain/tag.py

@@ -157,7 +157,7 @@ def tag_create(
             if tag_timezone is None:
                 tag_timezone = get_user_timezones()[1]
             elif isinstance(tag_timezone, str):
-                tag_timezone = parse_timezone(tag_timezone.encode())
+                tag_timezone, _ = parse_timezone(tag_timezone.encode())
             tag_obj.tag_timezone = tag_timezone
 
             # Check if we should sign the tag

+ 226 - 0
tests/porcelain/test_submodule.py

@@ -0,0 +1,226 @@
+# test_submodule.py -- tests for porcelain submodule functions
+# Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as published by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for porcelain submodule functions."""
+
+import os
+import shutil
+import tempfile
+
+from dulwich import porcelain
+from dulwich.config import ConfigFile
+from dulwich.objects import Blob, Commit, Tree
+from dulwich.repo import Repo
+
+from .. import TestCase
+
+
+class SubmoduleAddTests(TestCase):
+    """Tests for submodule_add function."""
+
+    def setUp(self):
+        super().setUp()
+        self.test_dir = tempfile.mkdtemp()
+        self.repo_path = os.path.join(self.test_dir, "repo")
+        self.repo = Repo.init(self.repo_path, mkdir=True)
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+        super().tearDown()
+
+    def test_submodule_add_basic(self) -> None:
+        """Test basic submodule_add with URL."""
+        url = "https://github.com/dulwich/dulwich.git"
+        path = "libs/dulwich"
+
+        porcelain.submodule_add(self.repo, url, path, name="dulwich")
+
+        # Check that .gitmodules was created
+        gitmodules_path = os.path.join(self.repo_path, ".gitmodules")
+        self.assertTrue(os.path.exists(gitmodules_path))
+
+        # Check .gitmodules content
+        config = ConfigFile.from_path(gitmodules_path)
+        self.assertEqual(url.encode(), config.get(("submodule", "dulwich"), "url"))
+        self.assertEqual(path.encode(), config.get(("submodule", "dulwich"), "path"))
+
+    def test_submodule_add_without_name(self) -> None:
+        """Test submodule_add derives name from path when not specified."""
+        url = "https://github.com/dulwich/dulwich.git"
+        path = "libs/dulwich"
+
+        porcelain.submodule_add(self.repo, url, path)
+
+        # Check that .gitmodules was created
+        gitmodules_path = os.path.join(self.repo_path, ".gitmodules")
+        config = ConfigFile.from_path(gitmodules_path)
+
+        # Name should be derived from path
+        self.assertEqual(url.encode(), config.get(("submodule", "libs/dulwich"), "url"))
+
+    def test_submodule_add_without_path(self) -> None:
+        """Test submodule_add derives path from URL when not specified."""
+        url = "https://github.com/dulwich/dulwich.git"
+
+        porcelain.submodule_add(self.repo, url)
+
+        # Check that .gitmodules was created
+        gitmodules_path = os.path.join(self.repo_path, ".gitmodules")
+        config = ConfigFile.from_path(gitmodules_path)
+
+        # Path should be derived from URL (just "dulwich")
+        # The actual value depends on _canonical_part implementation
+        # We just check that something was written
+        sections = list(config.keys())
+        self.assertEqual(1, len([s for s in sections if s[0] == b"submodule"]))
+
+    def test_submodule_add_updates_existing_gitmodules(self) -> None:
+        """Test that submodule_add updates existing .gitmodules file."""
+        # Add first submodule
+        url1 = "https://github.com/dulwich/dulwich.git"
+        path1 = "libs/dulwich"
+        porcelain.submodule_add(self.repo, url1, path1, name="dulwich")
+
+        # Add second submodule
+        url2 = "https://github.com/dulwich/dulwich-tests.git"
+        path2 = "libs/tests"
+        porcelain.submodule_add(self.repo, url2, path2, name="tests")
+
+        # Check both submodules are in .gitmodules
+        gitmodules_path = os.path.join(self.repo_path, ".gitmodules")
+        config = ConfigFile.from_path(gitmodules_path)
+
+        self.assertEqual(url1.encode(), config.get(("submodule", "dulwich"), "url"))
+        self.assertEqual(url2.encode(), config.get(("submodule", "tests"), "url"))
+
+
+class SubmoduleInitTests(TestCase):
+    """Tests for submodule_init function."""
+
+    def setUp(self):
+        super().setUp()
+        self.test_dir = tempfile.mkdtemp()
+        self.repo_path = os.path.join(self.test_dir, "repo")
+        self.repo = Repo.init(self.repo_path, mkdir=True)
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+        super().tearDown()
+
+    def test_submodule_init(self) -> None:
+        """Test submodule_init reads from .gitmodules and updates config."""
+        # Create .gitmodules file
+        gitmodules_path = os.path.join(self.repo_path, ".gitmodules")
+        config = ConfigFile()
+        config.set(
+            ("submodule", "dulwich"), "url", "https://github.com/dulwich/dulwich.git"
+        )
+        config.set(("submodule", "dulwich"), "path", "libs/dulwich")
+        config.path = gitmodules_path
+        config.write_to_path()
+
+        # Initialize submodules
+        porcelain.submodule_init(self.repo)
+
+        # Check that repo config was updated
+        repo_config = self.repo.get_config()
+        self.assertEqual(
+            b"true", repo_config.get((b"submodule", b"dulwich"), b"active")
+        )
+        self.assertEqual(
+            b"https://github.com/dulwich/dulwich.git",
+            repo_config.get((b"submodule", b"dulwich"), b"url"),
+        )
+
+    def test_submodule_init_no_gitmodules(self) -> None:
+        """Test submodule_init raises FileNotFoundError when .gitmodules is missing."""
+        # Should raise FileNotFoundError when .gitmodules doesn't exist
+        with self.assertRaises(FileNotFoundError):
+            porcelain.submodule_init(self.repo)
+
+
+class SubmoduleListTests(TestCase):
+    """Tests for submodule_list function."""
+
+    def setUp(self):
+        super().setUp()
+        self.test_dir = tempfile.mkdtemp()
+        self.repo_path = os.path.join(self.test_dir, "repo")
+        self.repo = Repo.init(self.repo_path, mkdir=True)
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+        super().tearDown()
+
+    def test_submodule_list_empty(self) -> None:
+        """Test submodule_list with no submodules."""
+        # Create an initial commit
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test.txt", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = commit.committer = b"Test User <test@example.com>"
+        commit.author_time = commit.commit_time = 1234567890
+        commit.author_timezone = commit.commit_timezone = 0
+        commit.encoding = b"UTF-8"
+        commit.message = b"Initial commit"
+        self.repo.object_store.add_object(commit)
+
+        self.repo.refs[b"refs/heads/main"] = commit.id
+        self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/main")
+
+        # List should be empty
+        submodules = list(porcelain.submodule_list(self.repo))
+        self.assertEqual([], submodules)
+
+    def test_submodule_list_with_submodule(self) -> None:
+        """Test submodule_list with a submodule in the tree."""
+        # Create a tree with a submodule entry
+        tree = Tree()
+        tree.add(b"test.txt", 0o100644, b"a" * 40)  # Dummy file
+        # Add a submodule entry with gitlink mode (0o160000)
+        submodule_sha = b"1" * 40
+        tree.add(b"libs/mylib", 0o160000, submodule_sha)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = commit.committer = b"Test User <test@example.com>"
+        commit.author_time = commit.commit_time = 1234567890
+        commit.author_timezone = commit.commit_timezone = 0
+        commit.encoding = b"UTF-8"
+        commit.message = b"Add submodule"
+        self.repo.object_store.add_object(commit)
+
+        self.repo.refs[b"refs/heads/main"] = commit.id
+        self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/main")
+
+        # List should contain the submodule
+        submodules = list(porcelain.submodule_list(self.repo))
+        self.assertEqual(1, len(submodules))
+        path, sha = submodules[0]
+        self.assertEqual("libs/mylib", path)
+        self.assertEqual(submodule_sha.decode(), sha)

+ 253 - 0
tests/porcelain/test_tag.py

@@ -0,0 +1,253 @@
+# test_tag.py -- tests for porcelain tag functions
+# Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as published by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for porcelain tag functions."""
+
+import os
+import shutil
+import tempfile
+
+from dulwich import porcelain
+from dulwich.objects import Blob, Commit, Tree
+from dulwich.repo import Repo
+
+from .. import TestCase
+
+
+class TagDeleteTests(TestCase):
+    """Tests for tag_delete function."""
+
+    def setUp(self):
+        super().setUp()
+        self.test_dir = tempfile.mkdtemp()
+        self.repo_path = os.path.join(self.test_dir, "repo")
+        self.repo = Repo.init(self.repo_path, mkdir=True)
+
+        # Create a simple commit to tag
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test.txt", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = commit.committer = b"Test User <test@example.com>"
+        commit.author_time = commit.commit_time = 1234567890
+        commit.author_timezone = commit.commit_timezone = 0
+        commit.encoding = b"UTF-8"
+        commit.message = b"Test commit"
+        self.repo.object_store.add_object(commit)
+
+        self.repo.refs[b"refs/heads/main"] = commit.id
+        self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/main")
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+        super().tearDown()
+
+    def test_tag_delete_with_bytes(self) -> None:
+        """Test tag_delete with bytes tag name."""
+        # Create a tag
+        porcelain.tag_create(self.repo, b"test-tag", annotated=False)
+        self.assertIn(b"refs/tags/test-tag", self.repo.refs)
+
+        # Delete the tag with bytes
+        porcelain.tag_delete(self.repo, b"test-tag")
+        self.assertNotIn(b"refs/tags/test-tag", self.repo.refs)
+
+    def test_tag_delete_invalid_type(self) -> None:
+        """Test tag_delete with invalid input type raises Error."""
+        from dulwich.porcelain import Error
+
+        # Create a tag
+        porcelain.tag_create(self.repo, b"test-tag", annotated=False)
+
+        # Try to delete with invalid type (should raise Error)
+        with self.assertRaises(Error) as cm:
+            porcelain.tag_delete(self.repo, 123)  # type: ignore
+
+        self.assertEqual("Unexpected tag name type 123", str(cm.exception))
+
+
+class TagCreateWithEncodingTests(TestCase):
+    """Tests for tag_create with encoding parameters."""
+
+    def setUp(self):
+        super().setUp()
+        self.test_dir = tempfile.mkdtemp()
+        self.repo_path = os.path.join(self.test_dir, "repo")
+        self.repo = Repo.init(self.repo_path, mkdir=True)
+
+        # Create a simple commit
+        blob = Blob.from_string(b"test content")
+        self.repo.object_store.add_object(blob)
+
+        tree = Tree()
+        tree.add(b"test.txt", 0o100644, blob.id)
+        self.repo.object_store.add_object(tree)
+
+        commit = Commit()
+        commit.tree = tree.id
+        commit.author = commit.committer = b"Test User <test@example.com>"
+        commit.author_time = commit.commit_time = 1234567890
+        commit.author_timezone = commit.commit_timezone = 0
+        commit.encoding = b"UTF-8"
+        commit.message = b"Test commit"
+        self.repo.object_store.add_object(commit)
+
+        self.repo.refs[b"refs/heads/main"] = commit.id
+        self.repo.refs.set_symbolic_ref(b"HEAD", b"refs/heads/main")
+
+    def tearDown(self):
+        shutil.rmtree(self.test_dir)
+        super().tearDown()
+
+    def test_tag_create_with_string_tag_name(self) -> None:
+        """Test tag_create with string tag name gets encoded."""
+        porcelain.tag_create(self.repo, "my-tag", annotated=True, message="Tag message")
+
+        # Verify tag was created
+        self.assertIn(b"refs/tags/my-tag", self.repo.refs)
+
+    def test_tag_create_with_string_author(self) -> None:
+        """Test tag_create with string author gets encoded."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author="Test Author <author@example.com>",
+            annotated=True,
+            message=b"Test message",
+        )
+
+        # Verify tag was created
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(b"Test Author <author@example.com>", tag_obj.tagger)
+
+    def test_tag_create_with_bytes_author(self) -> None:
+        """Test tag_create with bytes author."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test Author <author@example.com>",
+            annotated=True,
+            message=b"Test message",
+        )
+
+        # Verify tag was created
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(b"Test Author <author@example.com>", tag_obj.tagger)
+
+    def test_tag_create_with_string_message(self) -> None:
+        """Test tag_create with string message gets encoded."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test <test@example.com>",
+            annotated=True,
+            message="This is a test message",
+        )
+
+        # Verify tag was created with message
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(b"This is a test message\n", tag_obj.message)
+
+    def test_tag_create_with_bytes_message(self) -> None:
+        """Test tag_create with bytes message."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test <test@example.com>",
+            annotated=True,
+            message=b"This is a test message",
+        )
+
+        # Verify tag was created with message
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(b"This is a test message\n", tag_obj.message)
+
+    def test_tag_create_with_none_message(self) -> None:
+        """Test tag_create with None message defaults to empty."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test <test@example.com>",
+            annotated=True,
+            message=None,
+        )
+
+        # Verify tag was created with empty message
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(b"\n", tag_obj.message)
+
+    def test_tag_create_with_explicit_tag_time(self) -> None:
+        """Test tag_create with explicit tag_time."""
+        tag_time = 1700000000
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test <test@example.com>",
+            annotated=True,
+            message=b"Test",
+            tag_time=tag_time,
+        )
+
+        # Verify tag time is set correctly
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        self.assertEqual(tag_time, tag_obj.tag_time)
+
+    def test_tag_create_with_string_timezone(self) -> None:
+        """Test tag_create with string timezone."""
+        porcelain.tag_create(
+            self.repo,
+            b"test-tag",
+            author=b"Test <test@example.com>",
+            annotated=True,
+            message=b"Test",
+            tag_timezone="+0200",
+        )
+
+        # Verify tag was created with timezone
+        tag_ref = self.repo.refs[b"refs/tags/test-tag"]
+        tag_obj = self.repo[tag_ref]
+        # +0200 is 2 * 60 * 60 = 7200 seconds
+        self.assertEqual(7200, tag_obj.tag_timezone)
+
+    def test_tag_create_with_custom_encoding(self) -> None:
+        """Test tag_create with custom encoding."""
+        porcelain.tag_create(
+            self.repo,
+            "test-tag",
+            author="Test <test@example.com>",
+            annotated=True,
+            message="Test message",
+            encoding="utf-8",
+        )
+
+        # Verify tag was created
+        self.assertIn(b"refs/tags/test-tag", self.repo.refs)

+ 80 - 0
tests/test___main__.py

@@ -0,0 +1,80 @@
+# test___main__.py -- tests for __main__.py
+# Copyright (C) 2025 Jelmer Vernooij <jelmer@jelmer.uk>
+#
+# SPDX-License-Identifier: Apache-2.0 OR GPL-2.0-or-later
+# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
+# General Public License as published by the Free Software Foundation; version 2.0
+# or (at your option) any later version. You can redistribute it and/or
+# modify it under the terms of either of these two licenses.
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# You should have received a copy of the licenses; if not, see
+# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
+# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
+# License, Version 2.0.
+#
+
+"""Tests for __main__.py module entry point."""
+
+import subprocess
+import sys
+
+from . import TestCase
+
+
+class MainModuleTests(TestCase):
+    """Tests for the __main__.py module entry point."""
+
+    def test_main_module_help_flag(self) -> None:
+        """Test that running dulwich as a module with --help works."""
+        # Run dulwich as a module using python -m
+        result = subprocess.run(
+            [sys.executable, "-m", "dulwich", "--help"],
+            capture_output=True,
+            text=True,
+        )
+
+        # Help command exits with code 1 (standard behavior when no command is given)
+        self.assertEqual(1, result.returncode)
+
+        # Should start with usage line
+        self.assertTrue(result.stdout.startswith("usage: dulwich"))
+
+    def test_main_module_help_command(self) -> None:
+        """Test that running dulwich as a module with help command works."""
+        result = subprocess.run(
+            [sys.executable, "-m", "dulwich", "help"],
+            capture_output=True,
+            text=True,
+        )
+
+        # Help command should succeed
+        self.assertEqual(0, result.returncode)
+
+        # Check exact output (help goes to stderr)
+        expected = (
+            "The dulwich command line tool is currently a very basic frontend for the\n"
+            "Dulwich python module. For full functionality, please see the API reference.\n"
+            "\n"
+            "For a list of supported commands, see 'dulwich help -a'.\n"
+        )
+        self.assertEqual(expected, result.stderr)
+
+    def test_main_module_no_args(self) -> None:
+        """Test that running dulwich as a module with no arguments shows help."""
+        result = subprocess.run(
+            [sys.executable, "-m", "dulwich"],
+            capture_output=True,
+            text=True,
+        )
+
+        # No arguments should show help and exit with code 1
+        self.assertEqual(1, result.returncode)
+
+        # Should start with usage line
+        self.assertTrue(result.stdout.startswith("usage: dulwich"))

+ 100 - 0
tests/test_log_utils.py

@@ -246,3 +246,103 @@ class LogUtilsTests(TestCase):
 
         # Check that the level is DEBUG when tracing is enabled
         self.assertEqual(root_logger.level, logging.DEBUG)
+
+    def test_configure_logging_from_trace_file_path(self) -> None:
+        """Test _configure_logging_from_trace with file path."""
+        from dulwich.log_utils import _configure_logging_from_trace
+
+        # Save current root logger state
+        root_logger = logging.getLogger()
+        original_level = root_logger.level
+        original_handlers = list(root_logger.handlers)
+
+        def cleanup() -> None:
+            root_logger.handlers = original_handlers
+            root_logger.level = original_level
+
+        self.addCleanup(cleanup)
+
+        # Reset root logger
+        root_logger.handlers = []
+        root_logger.level = logging.WARNING
+
+        # Create a temporary file for trace output
+        with tempfile.NamedTemporaryFile(mode="w", delete=False) as f:
+            trace_file = f.name
+
+        try:
+            self._set_git_trace(trace_file)
+            result = _configure_logging_from_trace()
+
+            # Should succeed
+            self.assertTrue(result)
+
+            # Check that the root logger has a handler
+            self.assertTrue(root_logger.handlers)
+            self.assertEqual(root_logger.level, logging.DEBUG)
+        finally:
+            if os.path.exists(trace_file):
+                os.unlink(trace_file)
+
+    def test_configure_logging_from_trace_directory(self) -> None:
+        """Test _configure_logging_from_trace with directory path."""
+        from dulwich.log_utils import _configure_logging_from_trace
+
+        # Save current root logger state
+        root_logger = logging.getLogger()
+        original_level = root_logger.level
+        original_handlers = list(root_logger.handlers)
+
+        def cleanup() -> None:
+            root_logger.handlers = original_handlers
+            root_logger.level = original_level
+
+        self.addCleanup(cleanup)
+
+        # Reset root logger
+        root_logger.handlers = []
+        root_logger.level = logging.WARNING
+
+        # Create a temporary directory for trace output
+        with tempfile.TemporaryDirectory() as tmpdir:
+            self._set_git_trace(tmpdir)
+            result = _configure_logging_from_trace()
+
+            # Should succeed
+            self.assertTrue(result)
+
+            # Check that the root logger has a handler
+            self.assertTrue(root_logger.handlers)
+            self.assertEqual(root_logger.level, logging.DEBUG)
+
+            # Check that a file was created in the directory
+            trace_file = os.path.join(tmpdir, f"trace.{os.getpid()}")
+            # The file should exist after some logging
+            root_logger.debug("Test message")
+            self.assertTrue(os.path.exists(trace_file))
+
+    def test_configure_logging_from_trace_invalid_file(self) -> None:
+        """Test _configure_logging_from_trace with invalid file path."""
+        from dulwich.log_utils import _configure_logging_from_trace
+
+        # Save current root logger state
+        root_logger = logging.getLogger()
+        original_level = root_logger.level
+        original_handlers = list(root_logger.handlers)
+
+        def cleanup() -> None:
+            root_logger.handlers = original_handlers
+            root_logger.level = original_level
+
+        self.addCleanup(cleanup)
+
+        # Reset root logger
+        root_logger.handlers = []
+        root_logger.level = logging.WARNING
+
+        # Use an invalid path (directory that doesn't exist)
+        self._set_git_trace("/nonexistent/path/trace.log")
+        result = _configure_logging_from_trace()
+
+        # Should fail
+        self.assertFalse(result)