Skip to content
Open
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
131 changes: 131 additions & 0 deletions tests/unit/test_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# SPDX-FileCopyrightText: 2026 Knitli Inc.
#
# SPDX-License-Identifier: MIT OR Apache-2.0

import importlib.metadata
import shutil
import subprocess
import sys

from unittest.mock import MagicMock

import pytest

import codeweaver

Comment on lines +5 to +15

@pytest.fixture
def mock_no_version_file(monkeypatch):
"""Remove _version module from sys.modules and codeweaver module.
Comment on lines +17 to +19

A reload of the codeweaver module is necessary because the `get_version()`
function evaluates the imports (which populate its internal values) exactly once
at execution time. If we don't reload the module after changing sys.modules,
it may continue using the previously cached module references.
Comment on lines +21 to +24
"""
monkeypatch.delitem(sys.modules, "codeweaver._version", raising=False)
if hasattr(codeweaver, "_version"):
monkeypatch.delattr(codeweaver, "_version", raising=False)

# We must also clear the internal import mechanisms tracking caching!
# A cleaner approach is simply to re-import get_version explicitly so it bounds to the current env
import importlib

importlib.reload(codeweaver)
return codeweaver.get_version


@pytest.fixture
def mock_no_metadata(monkeypatch, mock_no_version_file):
"""Mock importlib.metadata to raise PackageNotFoundError for code-weaver."""
_original_version = importlib.metadata.version

def mock_version(pkg_name):
if pkg_name == "code-weaver":
raise importlib.metadata.PackageNotFoundError("code-weaver")
return _original_version(pkg_name)

monkeypatch.setattr(importlib.metadata, "version", mock_version)
Comment on lines +43 to +48
return mock_no_version_file


def test_get_version_from_version_file(monkeypatch, mock_no_version_file):
"""Test getting version from codeweaver._version.__version__."""
mock_module = MagicMock()
Comment on lines +52 to +54
mock_module.__version__ = "1.2.3-file"
monkeypatch.setitem(sys.modules, "codeweaver._version", mock_module)

# Need to reload codeweaver because we just changed sys.modules
import importlib

importlib.reload(codeweaver)

assert codeweaver.get_version() == "1.2.3-file"


def test_get_version_from_importlib_metadata(monkeypatch, mock_no_version_file):
"""Test getting version from importlib.metadata."""
get_version = mock_no_version_file
_original_version = importlib.metadata.version

def mock_version(pkg_name):
if pkg_name == "code-weaver":
return "1.2.3-metadata"
return _original_version(pkg_name)

monkeypatch.setattr(importlib.metadata, "version", mock_version)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Copilot reviewer is correct about this issue. The problem is that get_version() uses lazy imports - it imports importlib.metadata, shutil, and subprocess inside the function at runtime (see lines 46, 51-52 in src/codeweaver/init.py:37-72).

When you patch shutil.which or importlib.metadata.version at the stdlib module level, the import shutil statement inside get_version() creates a fresh local reference that may not see your monkeypatch.

The fix: Patch these imports where they're actually used. Since get_version() is in codeweaver module, you need to patch them there. However, since these are lazy imports (imported inside the function), the best approach is to:

  1. For importlib.metadata.version: Patch it at importlib.metadata.version level (this should work since it's called as importlib.metadata.version())
  2. For shutil.which and subprocess.run: These are trickier because they're imported inside the function

Recommended approach:
Use unittest.mock.patch with the actual import path. For lazy imports inside functions, you need to patch at the point where the module is imported. Here's an example pattern:

from unittest.mock import patch

def test_get_version_from_git_success():
    with patch('codeweaver._version.__version__', side_effect=ImportError):
        with patch('importlib.metadata.version', side_effect=importlib.metadata.PackageNotFoundError):
            with patch('shutil.which', return_value='/usr/bin/git'):
                with patch('subprocess.run') as mock_run:
                    mock_run.return_value.returncode = 0
                    mock_run.return_value.stdout = '1.2.3-git\n'
                    assert get_version() == '1.2.3-git'

Alternatively, since the imports happen at runtime inside the function, you could use monkeypatch.setattr on the actual module objects before they're imported, or use importlib machinery to ensure your mocks are in place.

The key insight: lazy imports (imports inside functions) require different mocking strategies than module-level imports.


assert get_version() == "1.2.3-metadata"


def test_get_version_from_git_success(monkeypatch, mock_no_metadata):
"""Test getting version from git describe."""
get_version = mock_no_metadata
monkeypatch.setattr(shutil, "which", lambda cmd: "git" if cmd == "git" else None)

mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = "1.2.3-git\n"

def mock_run(*args, **kwargs):
return mock_result

monkeypatch.setattr(subprocess, "run", mock_run)
Comment on lines +90 to +93

assert get_version() == "1.2.3-git"


def test_get_version_from_git_failure(monkeypatch, mock_no_metadata):
"""Test git describe fails and returns 0.0.0."""
get_version = mock_no_metadata
monkeypatch.setattr(shutil, "which", lambda cmd: "git" if cmd == "git" else None)

mock_result = MagicMock()
mock_result.returncode = 1

def mock_run(*args, **kwargs):
return mock_result

monkeypatch.setattr(subprocess, "run", mock_run)
Comment on lines +106 to +109

assert get_version() == "0.0.0"


def test_get_version_no_git(monkeypatch, mock_no_metadata):
"""Test git is not installed, returns 0.0.0."""
get_version = mock_no_metadata
monkeypatch.setattr(shutil, "which", lambda cmd: None)

assert get_version() == "0.0.0"


def test_get_version_exception(monkeypatch, mock_no_metadata):
"""Test general exception in git block returns 0.0.0."""
get_version = mock_no_metadata

def mock_which(cmd):
raise RuntimeError("Something went wrong")

monkeypatch.setattr(shutil, "which", mock_which)

assert get_version() == "0.0.0"
Loading