From 5773c70e90d37c6b0bf051bec87868cb5470497f Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 21 Jan 2026 15:27:42 +0800 Subject: [PATCH 1/4] feat: add `log_show_mode` parameter to control log display --- py_node_manager/manager.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/py_node_manager/manager.py b/py_node_manager/manager.py index 0ca3f3b..c6fd6ac 100644 --- a/py_node_manager/manager.py +++ b/py_node_manager/manager.py @@ -5,7 +5,7 @@ import tarfile import urllib.request import zipfile -from typing import Dict, Optional, Tuple +from typing import Dict, Literal, Optional, Tuple from .logger import get_logger @@ -17,7 +17,13 @@ class NodeManager: Node.js manager class """ - def __init__(self, download_node: bool, node_version: str, is_cli: bool = False): + def __init__( + self, + download_node: bool, + node_version: str, + is_cli: bool = False, + log_show_mode: Literal['all', 'slim', 'hide'] = 'all', + ): """ Node.js manager class @@ -29,6 +35,7 @@ def __init__(self, download_node: bool, node_version: str, is_cli: bool = False) self.download_node = download_node self.node_version = node_version self.is_cli = is_cli + self.log_show_mode = log_show_mode self.node_path = self._node_path() self.node_env = self._node_env() self.npm_path = self._npm_path() @@ -86,7 +93,7 @@ def download_nodejs(self) -> str: # Create directory for downloaded Node.js within the package directory # Use the package directory instead of current working directory - # Get the directory of this utils.py file + # Get the directory of this file package_dir = os.path.dirname(os.path.abspath(__file__)) node_dir_path = os.path.join(package_dir, '.nodejs_cache') if not os.path.exists(node_dir_path): @@ -100,18 +107,20 @@ def download_nodejs(self) -> str: # If Node.js already exists, return the path without downloading if os.path.exists(node_executable): - logger.info(f'📦 Using cached Node.js from {node_executable}') + if self.log_show_mode in {'all', 'slim'}: + logger.info(f'📦 Using cached Node.js from {node_executable}') return node_executable # Download Node.js node_archive = os.path.join(node_dir_path, os.path.basename(node_url)) - logger.info('🌐 Node.js not found in PATH. Downloading Node.js...') - if self.is_cli: + if self.log_show_mode in {'all', 'slim'}: + logger.info('🌐 Node.js not found in PATH. Downloading Node.js...') + if self.log_show_mode == 'all' and self.is_cli: logger.info(f'📥 Downloading Node.js from {node_url}...') urllib.request.urlretrieve(node_url, node_archive) # Extract Node.js - if self.is_cli: + if self.log_show_mode == 'all' and self.is_cli: logger.info('🔧 Extracting Node.js...') if node_archive.endswith('.tar.gz'): with tarfile.open(node_archive, 'r:gz') as tar: @@ -130,7 +139,8 @@ def download_nodejs(self) -> str: if system != 'windows': os.chmod(node_executable, 0o755) - logger.info(f'✅ Node.js downloaded and extracted to {node_executable}') + if self.log_show_mode in {'all', 'slim'}: + logger.info(f'✅ Node.js downloaded and extracted to {node_executable}') return node_executable def check_or_download_nodejs(self) -> Optional[str]: @@ -143,7 +153,8 @@ def check_or_download_nodejs(self) -> Optional[str]: # First check if Node.js is available in PATH is_available, version = self.check_nodejs_available() if is_available: - logger.info(f'💻 Using System Default Node.js {version}') + if self.log_show_mode in {'all', 'slim'}: + logger.info(f'💻 Using System Default Node.js {version}') return None # Use system Node.js From 437eaddfec0588687a012ba918aceaa18af4ca7a Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 21 Jan 2026 15:33:23 +0800 Subject: [PATCH 2/4] refactor: refactor test cases --- tests/test_node_manager.py | 598 +++++++++++++++++++++---------------- 1 file changed, 334 insertions(+), 264 deletions(-) diff --git a/tests/test_node_manager.py b/tests/test_node_manager.py index c12a672..08cab20 100644 --- a/tests/test_node_manager.py +++ b/tests/test_node_manager.py @@ -1,100 +1,128 @@ +import logging +import os +from unittest.mock import call, patch + import pytest -from unittest.mock import patch, MagicMock +from py_node_manager.logger import ColoredFormatter +from py_node_manager.manager import NodeManager + + +@pytest.fixture +def mock_dependencies(): + """Fixture to mock common dependencies""" + with patch('py_node_manager.manager.platform.system') as mock_system, patch( + 'py_node_manager.manager.platform.machine' + ) as mock_machine, patch('py_node_manager.manager.os.path.dirname') as mock_dirname, patch( + 'py_node_manager.manager.os.path.abspath' + ) as mock_abspath, patch('py_node_manager.manager.os.path.exists') as mock_exists, patch( + 'py_node_manager.manager.os.makedirs' + ) as mock_makedirs, patch('py_node_manager.manager.os.chmod') as mock_chmod, patch( + 'py_node_manager.manager.os.remove' + ) as mock_remove, patch('py_node_manager.manager.urllib.request.urlretrieve') as mock_urlretrieve, patch( + 'py_node_manager.manager.tarfile.open' + ) as mock_tarfile, patch('py_node_manager.manager.zipfile.ZipFile') as mock_zipfile, patch( + 'py_node_manager.manager.logger' + ) as mock_logger, patch('py_node_manager.manager.subprocess.run') as mock_subprocess, patch( + 'py_node_manager.manager.os.path.join', side_effect=os.path.join + ) as mock_join: # Keep real join behavior by default + # Default setup + mock_dirname.return_value = '/test/path' + mock_abspath.return_value = '/test/path' + mock_system.return_value = 'Linux' + mock_machine.return_value = 'x86_64' -class TestNodeManager: - """Test cases for NodeManager class""" + yield { + 'system': mock_system, + 'machine': mock_machine, + 'dirname': mock_dirname, + 'abspath': mock_abspath, + 'exists': mock_exists, + 'makedirs': mock_makedirs, + 'chmod': mock_chmod, + 'remove': mock_remove, + 'urlretrieve': mock_urlretrieve, + 'tarfile': mock_tarfile, + 'zipfile': mock_zipfile, + 'logger': mock_logger, + 'subprocess': mock_subprocess, + 'join': mock_join, + } - def setup_method(self): - """Set up test fixtures before each test method.""" - # Import here to avoid issues with pytest - from py_node_manager.manager import NodeManager - self.NodeManager = NodeManager +class TestNodeManager: + """Test cases for NodeManager class""" @pytest.mark.parametrize('node_version', ['18.17.0', '20.10.0', '16.20.2']) def test_init_with_different_versions(self, node_version): """Test NodeManager initialization with different Node.js versions""" - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=False, node_version=node_version) + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version=node_version) assert manager.download_node is False assert manager.node_version == node_version assert manager.is_cli is False def test_init(self): """Test NodeManager initialization""" - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=False, node_version='18.17.0') + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version='18.17.0') assert manager.download_node is False assert manager.node_version == '18.17.0' assert manager.is_cli is False def test_init_with_cli(self): """Test NodeManager initialization with CLI flag""" - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=True, node_version='18.17.0', is_cli=True) + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=True, node_version='18.17.0', is_cli=True) assert manager.download_node is True assert manager.node_version == '18.17.0' assert manager.is_cli is True - @patch('py_node_manager.manager.subprocess.run') - def test_check_nodejs_available_success(self, mock_run): + def test_check_nodejs_available_success(self, mock_dependencies): """Test check_nodejs_available when Node.js is available""" - # Mock subprocess.run to simulate successful Node.js detection - mock_result = MagicMock() - mock_result.returncode = 0 - mock_result.stdout = 'v18.17.0\n' - mock_run.return_value = mock_result + mock_dependencies['subprocess'].return_value.returncode = 0 + mock_dependencies['subprocess'].return_value.stdout = 'v18.17.0\n' - manager = self.NodeManager(download_node=False, node_version='18.17.0') - is_available, version = manager.check_nodejs_available() + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version='18.17.0') + is_available, version = manager.check_nodejs_available() - assert is_available is True - assert version == 'v18.17.0' + assert is_available is True + assert version == 'v18.17.0' - def test_check_nodejs_available_not_found(self): + def test_check_nodejs_available_not_found(self, mock_dependencies): """Test check_nodejs_available when Node.js is not found""" - # Mock subprocess.run to simulate FileNotFoundError - from py_node_manager.manager import NodeManager + mock_dependencies['subprocess'].side_effect = FileNotFoundError - with patch('py_node_manager.manager.subprocess.run', side_effect=FileNotFoundError): - manager = NodeManager.__new__(NodeManager) # Create instance without calling __init__ - is_available, version = manager.check_nodejs_available() + manager = NodeManager.__new__(NodeManager) # Create instance without calling __init__ + is_available, version = manager.check_nodejs_available() - assert is_available is False - assert version == '' + assert is_available is False + assert version == '' - def test_check_nodejs_available_non_zero_return(self): + def test_check_nodejs_available_non_zero_return(self, mock_dependencies): """Test check_nodejs_available when Node.js command returns non-zero exit code""" - # Mock subprocess.run to simulate non-zero return code - mock_result = MagicMock() - mock_result.returncode = 1 - from py_node_manager.manager import NodeManager + mock_dependencies['subprocess'].return_value.returncode = 1 - with patch('py_node_manager.manager.subprocess.run', return_value=mock_result): - manager = NodeManager.__new__(NodeManager) # Create instance without calling __init__ - is_available, version = manager.check_nodejs_available() + manager = NodeManager.__new__(NodeManager) + is_available, version = manager.check_nodejs_available() - assert is_available is False - assert version == '' + assert is_available is False + assert version == '' @pytest.mark.parametrize('platform_name', ['Windows', 'Linux', 'Darwin']) - def test_get_command_alias_by_platform(self, platform_name): + def test_get_command_alias_by_platform(self, platform_name, mock_dependencies): """Test get_command_alias_by_platform on different platforms""" - # Mock platform.system() to return specific platform - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=False, node_version='18.17.0') - result = manager.get_command_alias_by_platform('npm') - - if platform_name == 'Windows': - assert result == 'npm.cmd' - else: - assert result == 'npm' + mock_dependencies['system'].return_value = platform_name + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version='18.17.0') + result = manager.get_command_alias_by_platform('npm') + + if platform_name == 'Windows': + assert result == 'npm.cmd' + else: + assert result == 'npm' @pytest.mark.parametrize( 'platform_name,machine,expected_url', @@ -106,98 +134,80 @@ def test_get_command_alias_by_platform(self, platform_name): ('Darwin', 'x86_64', 'https://nodejs.org/dist/v18.17.0/node-v18.17.0-darwin-x64.tar.gz'), ], ) - def test_download_nodejs_url_generation(self, platform_name, machine, expected_url): + def test_download_nodejs_url_generation(self, platform_name, machine, expected_url, mock_dependencies): """Test that download_nodejs generates correct URLs for different platforms""" - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - with patch('py_node_manager.manager.platform.machine', return_value=machine): - with patch('py_node_manager.manager.os.path.dirname', return_value='/test/path'): - with patch('py_node_manager.manager.os.path.abspath', return_value='/test/path'): - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=True, node_version='18.17.0') - # Mock the download method to avoid actual download - with patch('py_node_manager.manager.urllib.request.urlretrieve') as mock_urlretrieve: - with patch('py_node_manager.manager.os.makedirs'): - with patch('py_node_manager.manager.os.path.exists', return_value=False): - with patch('py_node_manager.manager.tarfile.open'): - with patch('py_node_manager.manager.zipfile.ZipFile'): - with patch('py_node_manager.manager.os.chmod'): - with patch('py_node_manager.manager.os.remove'): - try: - manager.download_nodejs() - except Exception: - pass # We're only interested in the URL generation - - # Check that urlretrieve was called with the correct URL - mock_urlretrieve.assert_called_once() - called_url = mock_urlretrieve.call_args[0][0] - assert called_url == expected_url - - @patch('py_node_manager.manager.platform.system') - @patch('py_node_manager.manager.platform.machine') - def test_download_nodejs_unsupported_platform(self, mock_machine, mock_system): - """Test download_nodejs with unsupported platform""" - mock_system.return_value = 'UnsupportedOS' - mock_machine.return_value = 'x86_64' + mock_dependencies['system'].return_value = platform_name + mock_dependencies['machine'].return_value = machine + mock_dependencies['exists'].return_value = False - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=True, node_version='18.17.0') + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=True, node_version='18.17.0') try: manager.download_nodejs() - assert False, 'Expected RuntimeError was not raised' - except RuntimeError as e: - assert 'Unsupported platform: unsupportedos' in str(e) + except Exception: + pass # We're only interested in the URL generation + + mock_dependencies['urlretrieve'].assert_called_once() + called_url = mock_dependencies['urlretrieve'].call_args[0][0] + assert called_url == expected_url + + def test_download_nodejs_unsupported_platform(self, mock_dependencies): + """Test download_nodejs with unsupported platform""" + mock_dependencies['system'].return_value = 'UnsupportedOS' + mock_dependencies['machine'].return_value = 'x86_64' + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=True, node_version='18.17.0') + + with pytest.raises(RuntimeError) as excinfo: + manager.download_nodejs() + assert 'Unsupported platform: unsupportedos' in str(excinfo.value) def test_node_path_property(self): """Test _node_path property""" - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: + with patch.object(NodeManager, 'check_or_download_nodejs') as mock_check: mock_check.return_value = '/path/to/node' - manager = self.NodeManager(download_node=True, node_version='18.17.0') + manager = NodeManager(download_node=True, node_version='18.17.0') assert manager.node_path == '/path/to/node' def test_node_env_property_with_node_path(self): """Test _node_env property when node_path is set""" - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: - mock_check.return_value = '/path/to/node' - manager = self.NodeManager(download_node=True, node_version='18.17.0') + with patch.object(NodeManager, 'check_or_download_nodejs', return_value='/path/to/node'): + manager = NodeManager(download_node=True, node_version='18.17.0') - # Mock os.environ.copy() to return a known dictionary - with patch('py_node_manager.manager.os.environ.copy', return_value={'PATH': '/usr/bin'}): - with patch('py_node_manager.manager.os.pathsep', ':'): - env = manager.node_env - assert env is not None - assert '/path/to' in env['PATH'] + with patch('py_node_manager.manager.os.environ.copy', return_value={'PATH': '/usr/bin'}), patch( + 'py_node_manager.manager.os.pathsep', ':' + ): + env = manager.node_env + assert env is not None + assert '/path/to' in env['PATH'] def test_node_env_property_without_node_path(self): """Test _node_env property when node_path is None""" - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: - mock_check.return_value = None - manager = self.NodeManager(download_node=False, node_version='18.17.0') + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version='18.17.0') assert manager.node_env is None - def test_npm_path_with_node_path(self): + def test_npm_path_with_node_path(self, mock_dependencies): """Test _npm_path when node_path is set""" - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: - mock_check.return_value = '/path/to/node' - with patch('py_node_manager.manager.os.path.dirname', return_value='/path/to'): - with patch('py_node_manager.manager.os.path.exists', return_value=True): - manager = self.NodeManager(download_node=True, node_version='18.17.0') - npm_path = manager.npm_path - # Should contain the directory path - assert '/path/to' in npm_path - - def test_npx_path_with_node_path(self): + mock_dependencies['dirname'].return_value = '/path/to' + mock_dependencies['exists'].return_value = True + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value='/path/to/node'): + manager = NodeManager(download_node=True, node_version='18.17.0') + npm_path = manager.npm_path + assert '/path/to' in npm_path + + def test_npx_path_with_node_path(self, mock_dependencies): """Test _npx_path when node_path is set""" - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: - mock_check.return_value = '/path/to/node' - with patch('py_node_manager.manager.os.path.dirname', return_value='/path/to'): - with patch('py_node_manager.manager.os.path.exists', return_value=True): - manager = self.NodeManager(download_node=True, node_version='18.17.0') - npx_path = manager.npx_path - # Should contain the directory path - assert '/path/to' in npx_path + mock_dependencies['dirname'].return_value = '/path/to' + mock_dependencies['exists'].return_value = True + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value='/path/to/node'): + manager = NodeManager(download_node=True, node_version='18.17.0') + npx_path = manager.npx_path + assert '/path/to' in npx_path @pytest.mark.parametrize( 'platform_name,expected_npm,expected_npx', @@ -207,14 +217,14 @@ def test_npx_path_with_node_path(self): ('Darwin', 'npm', 'npx'), ], ) - def test_command_paths_without_node_path(self, platform_name, expected_npm, expected_npx): + def test_command_paths_without_node_path(self, platform_name, expected_npm, expected_npx, mock_dependencies): """Test command paths when node_path is None on different platforms""" - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - with patch.object(self.NodeManager, 'check_or_download_nodejs') as mock_check: - mock_check.return_value = None - manager = self.NodeManager(download_node=False, node_version='18.17.0') - assert manager.npm_path == expected_npm - assert manager.npx_path == expected_npx + mock_dependencies['system'].return_value = platform_name + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=False, node_version='18.17.0') + assert manager.npm_path == expected_npm + assert manager.npx_path == expected_npx @pytest.mark.parametrize( 'platform_name,machine,expected_node_dir', @@ -226,166 +236,226 @@ def test_command_paths_without_node_path(self, platform_name, expected_npm, expe ('Windows', 'AMD64', 'node-v18.17.0-win-x64'), ], ) - def test_node_directory_name_generation(self, platform_name, machine, expected_node_dir): + def test_node_directory_name_generation(self, platform_name, machine, expected_node_dir, mock_dependencies): """Test that Node.js directory names are generated correctly for different platforms""" - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - with patch('py_node_manager.manager.platform.machine', return_value=machine): - with patch('py_node_manager.manager.os.path.dirname', return_value='/test/path'): - with patch('py_node_manager.manager.os.path.abspath', return_value='/test/path'): - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=True, node_version='18.17.0') - # Mock the download method to capture the node_dir value - with patch('py_node_manager.manager.urllib.request.urlretrieve'): - with patch('py_node_manager.manager.os.makedirs'): - with patch( - 'py_node_manager.manager.os.path.exists', return_value=False - ) as mock_exists: - with patch('py_node_manager.manager.tarfile.open'): - with patch('py_node_manager.manager.zipfile.ZipFile'): - with patch('py_node_manager.manager.os.chmod'): - with patch('py_node_manager.manager.os.remove'): - # Mock os.path.join to capture the node_dir parameter - with patch('py_node_manager.manager.os.path.join') as mock_join: - # Make join return a predictable value - mock_join.return_value = ( - f'/test/path/.nodejs_cache/{expected_node_dir}' - ) - try: - manager.download_nodejs() - except Exception: - pass # We're only interested in the directory name generation - - # Verify that the expected node directory name was used - # Check that os.path.exists was called with the correct path - expected_path = ( - f'/test/path/.nodejs_cache/{expected_node_dir}' - ) - mock_exists.assert_called_with(expected_path) - - def test_download_nodejs_cached_node(self): + mock_dependencies['system'].return_value = platform_name + mock_dependencies['machine'].return_value = machine + mock_dependencies['exists'].return_value = False + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=True, node_version='18.17.0') + + try: + manager.download_nodejs() + except Exception: + pass + + # Construct the expected executable path based on platform + # Note: os.path.join is real in our fixture for the most part + base_path = os.path.join('/test/path', '.nodejs_cache', expected_node_dir) + if platform_name == 'Windows': + expected_executable = os.path.join(base_path, 'node.exe') + else: + expected_executable = os.path.join(base_path, 'bin', 'node') + + mock_dependencies['exists'].assert_any_call(expected_executable) + + def test_download_nodejs_cached_node(self, mock_dependencies): """Test download_nodejs when Node.js is already cached""" - platform_name = 'Linux' - machine = 'x86_64' expected_node_dir = 'node-v18.17.0-linux-x64' expected_executable = f'/test/path/.nodejs_cache/{expected_node_dir}/bin/node' - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - with patch('py_node_manager.manager.platform.machine', return_value=machine): - with patch('py_node_manager.manager.os.path.dirname', return_value='/test/path'): - with patch('py_node_manager.manager.os.path.abspath', return_value='/test/path'): - # Mock the check_or_download_nodejs method to avoid actual Node.js checks - with patch.object(self.NodeManager, 'check_or_download_nodejs', return_value=None): - manager = self.NodeManager(download_node=True, node_version='18.17.0') - # Mock os.path.exists to return True for the executable path - with patch( - 'py_node_manager.manager.os.path.exists', - side_effect=lambda path: path == expected_executable, - ) as mock_exists: - with patch('py_node_manager.manager.logger') as mock_logger: - # Mock other methods to avoid actual download - with patch('py_node_manager.manager.urllib.request.urlretrieve'): - with patch('py_node_manager.manager.os.makedirs'): - with patch('py_node_manager.manager.tarfile.open'): - with patch('py_node_manager.manager.zipfile.ZipFile'): - with patch('py_node_manager.manager.os.chmod'): - with patch('py_node_manager.manager.os.remove'): - result = manager.download_nodejs() - - # Verify that the cached Node.js is used - assert result == expected_executable - # Verify that logger.info was called with the correct message - mock_logger.info.assert_called_with( - f'📦 Using cached Node.js from {expected_executable}' - ) - # Verify that os.path.exists was called with the correct path - mock_exists.assert_called_with(expected_executable) - - def test_download_nodejs_cli_mode_logs(self): + # Simulate that the node executable exists + mock_dependencies['exists'].side_effect = lambda path: path == expected_executable + + with patch.object(NodeManager, 'check_or_download_nodejs', return_value=None): + manager = NodeManager(download_node=True, node_version='18.17.0') + result = manager.download_nodejs() + + assert result == expected_executable + mock_dependencies['logger'].info.assert_called_with(f'📦 Using cached Node.js from {expected_executable}') + + def test_download_nodejs_cli_mode_logs(self, mock_dependencies): """Test download_nodejs CLI mode logs""" - platform_name = 'Linux' - machine = 'x86_64' expected_url = 'https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz' + mock_dependencies['exists'].return_value = False + + manager = NodeManager.__new__(NodeManager) + manager.download_node = True + manager.node_version = '18.17.0' + manager.is_cli = True + manager.log_show_mode = 'all' + + try: + manager.download_nodejs() + except Exception: + pass - with patch('py_node_manager.manager.platform.system', return_value=platform_name): - with patch('py_node_manager.manager.platform.machine', return_value=machine): - with patch('py_node_manager.manager.os.path.dirname', return_value='/test/path'): - with patch('py_node_manager.manager.os.path.abspath', return_value='/test/path'): - # Create manager with is_cli=True using direct instantiation to avoid __init__ issues - from py_node_manager.manager import NodeManager - - manager = NodeManager.__new__(NodeManager) - manager.download_node = True - manager.node_version = '18.17.0' - manager.is_cli = True - # Mock os.path.exists to return False to force download - with patch('py_node_manager.manager.os.path.exists', return_value=False): - with patch('py_node_manager.manager.logger') as mock_logger: - # Mock the download method - with patch('py_node_manager.manager.urllib.request.urlretrieve'): - with patch('py_node_manager.manager.os.makedirs'): - with patch('py_node_manager.manager.tarfile.open'): - with patch('py_node_manager.manager.zipfile.ZipFile'): - with patch('py_node_manager.manager.os.chmod'): - with patch('py_node_manager.manager.os.remove'): - try: - manager.download_nodejs() - except Exception: - pass # We're only interested in the logging - - # Verify that the CLI download log was called - mock_logger.info.assert_any_call( - f'📥 Downloading Node.js from {expected_url}...' - ) - # Verify that the CLI extraction log was called - mock_logger.info.assert_any_call('🔧 Extracting Node.js...') + mock_dependencies['logger'].info.assert_any_call(f'📥 Downloading Node.js from {expected_url}...') + mock_dependencies['logger'].info.assert_any_call('🔧 Extracting Node.js...') def test_check_or_download_nodejs_no_download_raises_error(self): """Test check_or_download_nodejs raises error when download_node=False and Node.js not found""" - with patch.object(self.NodeManager, 'check_nodejs_available', return_value=(False, '')): - # Create manager with download_node=False using direct instantiation to avoid __init__ issues - from py_node_manager.manager import NodeManager - + with patch.object(NodeManager, 'check_nodejs_available', return_value=(False, '')): manager = NodeManager.__new__(NodeManager) manager.download_node = False manager.node_version = '18.17.0' manager.is_cli = False - # Should raise RuntimeError - try: + + with pytest.raises(RuntimeError) as excinfo: manager.check_or_download_nodejs() - assert False, 'Expected RuntimeError was not raised' - except RuntimeError as e: - assert 'Node.js is required for offline mode but not found' in str(e) + assert 'Node.js is required for offline mode but not found' in str(excinfo.value) def test_check_or_download_nodejs_no_download_cli_raises_error(self): """Test check_or_download_nodejs raises error when download_node=False and is_cli=True and Node.js not found""" - with patch.object(self.NodeManager, 'check_nodejs_available', return_value=(False, '')): - # Create manager with download_node=False and is_cli=True using direct instantiation - from py_node_manager.manager import NodeManager - + with patch.object(NodeManager, 'check_nodejs_available', return_value=(False, '')): manager = NodeManager.__new__(NodeManager) manager.download_node = False manager.node_version = '18.17.0' manager.is_cli = True - # Should raise RuntimeError - try: + + with pytest.raises(RuntimeError) as excinfo: manager.check_or_download_nodejs() - assert False, 'Expected RuntimeError was not raised' - except RuntimeError as e: - assert 'Node.js is required but not found in PATH' in str(e) + assert 'Node.js is required but not found in PATH' in str(excinfo.value) def test_check_or_download_nodejs_returns_download_result(self): """Test check_or_download_nodejs returns the result of download_nodejs when Node.js is not found and download_node=True""" - with patch.object(self.NodeManager, 'check_nodejs_available', return_value=(False, '')): - # Create manager with download_node=True using direct instantiation - from py_node_manager.manager import NodeManager - + with patch.object(NodeManager, 'check_nodejs_available', return_value=(False, '')): manager = NodeManager.__new__(NodeManager) manager.download_node = True manager.node_version = '18.17.0' manager.is_cli = False - # Mock download_nodejs to return a specific path + expected_path = '/path/to/downloaded/node' with patch.object(manager, 'download_nodejs', return_value=expected_path): result = manager.check_or_download_nodejs() assert result == expected_path + + @pytest.mark.parametrize('mode', ['slim', 'hide']) + def test_download_nodejs_log_show_mode(self, mode, mock_dependencies): + """Test download_nodejs with different log_show_mode""" + expected_url = 'https://nodejs.org/dist/v18.17.0/node-v18.17.0-linux-x64.tar.xz' + mock_dependencies['exists'].return_value = False + + manager = NodeManager.__new__(NodeManager) + manager.download_node = True + manager.node_version = '18.17.0' + manager.is_cli = True + manager.log_show_mode = mode + + try: + manager.download_nodejs() + except Exception: + pass + + download_msg = f'📥 Downloading Node.js from {expected_url}...' + not_found_msg = '🌐 Node.js not found in PATH. Downloading Node.js...' + # The exact path depends on how os.path.join works in the fixture, but we can rely on what we know + node_dir = 'node-v18.17.0-linux-x64' + expected_executable = os.path.join('/test/path', '.nodejs_cache', node_dir, 'bin', 'node') + extracted_msg = f'✅ Node.js downloaded and extracted to {expected_executable}' + + if mode == 'slim': + mock_dependencies['logger'].info.assert_any_call(not_found_msg) + mock_dependencies['logger'].info.assert_any_call(extracted_msg) + + # Verify download_msg was NOT called + # call_args_list is a list of call objects + calls = mock_dependencies['logger'].info.call_args_list + assert call(download_msg) not in calls + + elif mode == 'hide': + mock_dependencies['logger'].info.assert_not_called() + + def test_use_system_nodejs_log_output(self, mock_dependencies): + """Test using system Node.js with logging enabled""" + # Configure subprocess mock to simulate installed Node.js + mock_dependencies['subprocess'].return_value.returncode = 0 + mock_dependencies['subprocess'].return_value.stdout = 'v14.17.0\n' + + # Create instance manually to control properties directly + manager = NodeManager.__new__(NodeManager) + manager.download_node = False + manager.node_version = '18.17.0' + manager.is_cli = False + manager.log_show_mode = 'all' + + # Call method directly + result = manager.check_or_download_nodejs() + + assert result is None + mock_dependencies['logger'].info.assert_called_with('💻 Using System Default Node.js v14.17.0') + + def test_check_nodejs_available_log_show_mode_hide(self, mock_dependencies): + """Test check_nodejs_available with log_show_mode='hide'""" + mock_dependencies['subprocess'].return_value.returncode = 0 + mock_dependencies['subprocess'].return_value.stdout = 'v18.17.0\n' + + manager = NodeManager.__new__(NodeManager) + manager.download_node = False + manager.node_version = '18.17.0' + manager.is_cli = False + manager.log_show_mode = 'hide' + + is_available, version = manager.check_nodejs_available() + + assert is_available is True + assert version == 'v18.17.0' + mock_dependencies['logger'].info.assert_not_called() + + @pytest.mark.parametrize('mode', ['slim', 'hide']) + def test_download_nodejs_cached_node_log_mode(self, mode, mock_dependencies): + """Test download_nodejs cached node with different log modes""" + expected_node_dir = 'node-v18.17.0-linux-x64' + expected_executable = f'/test/path/.nodejs_cache/{expected_node_dir}/bin/node' + + # Override exists to find the cached node + mock_dependencies['exists'].side_effect = lambda path: path == expected_executable + + manager = NodeManager.__new__(NodeManager) + manager.download_node = True + manager.node_version = '18.17.0' + manager.is_cli = True + manager.log_show_mode = mode + + result = manager.download_nodejs() + + assert result == expected_executable + msg = f'📦 Using cached Node.js from {expected_executable}' + + if mode == 'slim': + mock_dependencies['logger'].info.assert_called_with(msg) + elif mode == 'hide': + mock_dependencies['logger'].info.assert_not_called() + + +def test_logger_formatter(): + """Test ColoredFormatter""" + formatter = ColoredFormatter('%(levelname)s: %(message)s') + record = logging.LogRecord( + name='test', level=logging.INFO, pathname='test.py', lineno=10, msg='test message', args=(), exc_info=None + ) + record.funcName = 'test_func' + + formatted = formatter.format(record) + + # Check for color codes + assert '\033[32mINFO\033[0m' in formatted # Green for INFO + assert 'test message' in formatted + + # Test other levels + # Create new record to avoid side effects from previous formatting + + record_error = logging.LogRecord( + name='test', level=logging.ERROR, pathname='test.py', lineno=10, msg='error message', args=(), exc_info=None + ) + record_error.funcName = 'test_func' + formatted = formatter.format(record_error) + assert '\033[31mERROR\033[0m' in formatted # Red for ERROR + + record_debug = logging.LogRecord( + name='test', level=logging.DEBUG, pathname='test.py', lineno=10, msg='debug message', args=(), exc_info=None + ) + record_debug.funcName = 'test_func' + formatted = formatter.format(record_debug) + assert '\033[36mDEBUG\033[0m' in formatted # Cyan for DEBUG From 847aa1e5d0aef9ea0a6f36a426c5d7eddcb6e5fe Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 21 Jan 2026 15:34:24 +0800 Subject: [PATCH 3/4] chore: update the ruff configuration file --- ruff.toml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index b5608c4..8e3db78 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,19 @@ +line-length = 120 +show-fixes = true target-version = "py38" -line-length = 120 +[lint] +select = [ + "I", # isort + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes +] +ignore = [ + "E501", # line too long + "W191", # indentation contains tabs +] [format] +docstring-code-format = true quote-style = "single" \ No newline at end of file From 22dd248a585f68be3994f5c806ceb3d7761966ba Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Wed, 21 Jan 2026 15:34:50 +0800 Subject: [PATCH 4/4] style: fix lint and format --- py_node_manager/__init__.py | 1 - py_node_manager/manager.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/py_node_manager/__init__.py b/py_node_manager/__init__.py index f5f4986..9c145e1 100644 --- a/py_node_manager/__init__.py +++ b/py_node_manager/__init__.py @@ -1,7 +1,6 @@ from .logger import get_logger from .manager import NodeManager - __version__ = '0.1.1' diff --git a/py_node_manager/manager.py b/py_node_manager/manager.py index c6fd6ac..c7bd863 100644 --- a/py_node_manager/manager.py +++ b/py_node_manager/manager.py @@ -6,8 +6,8 @@ import urllib.request import zipfile from typing import Dict, Literal, Optional, Tuple -from .logger import get_logger +from .logger import get_logger logger = get_logger(logging.getLogger(__name__))