Skip to content

Commit 846bc8f

Browse files
committed
✅ Increase test coverage for logger, prerequisites, and pythonpackage
Add comprehensive tests to improve coverage: Logger module (66% → 86%): - Color setup and configuration (never/always/auto modes) - Utility functions (shorten_string, get_console_width) - LevelDifferentiatingFormatter for all log levels - shprint error handling with filters and critical failures - Logging helpers (info_main, info_notify) Prerequisites module (45% → 80%): - Base Prerequisite class methods (is_valid, checker) - Installation workflow (ask_to_install, install) - JDK version checking and JAVA_HOME support - Homebrew formula location helpers - Main check_and_install workflow Pythonpackage module: - Parametrized tests for parse_as_folder_reference edge cases - Filesystem path detection for relative paths and git URLs - Dependency transformation with query params and fragments - Error handling for package extraction and invalid metadata
1 parent 0155c7e commit 846bc8f

File tree

3 files changed

+609
-1
lines changed

3 files changed

+609
-1
lines changed

tests/test_logger.py

Lines changed: 217 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,224 @@
1+
import logging
2+
import sh
3+
import pytest
14
import unittest
2-
from unittest.mock import MagicMock
5+
from unittest.mock import MagicMock, Mock, patch
36
from pythonforandroid import logger
47

58

9+
class TestColorSetup:
10+
"""Test color setup and configuration."""
11+
12+
def test_setup_color_never(self):
13+
"""Test color disabled when set to 'never'."""
14+
logger.setup_color('never')
15+
assert not logger.Out_Style._enabled
16+
assert not logger.Out_Fore._enabled
17+
assert not logger.Err_Style._enabled
18+
assert not logger.Err_Fore._enabled
19+
20+
def test_setup_color_always(self):
21+
"""Test color enabled when set to 'always'."""
22+
logger.setup_color('always')
23+
assert logger.Out_Style._enabled
24+
assert logger.Out_Fore._enabled
25+
assert logger.Err_Style._enabled
26+
assert logger.Err_Fore._enabled
27+
28+
@patch('pythonforandroid.logger.stdout')
29+
@patch('pythonforandroid.logger.stderr')
30+
def test_setup_color_auto_with_tty(self, mock_stderr, mock_stdout):
31+
"""Test color enabled when auto and isatty() returns True."""
32+
mock_stdout.isatty.return_value = True
33+
mock_stderr.isatty.return_value = True
34+
logger.setup_color('auto')
35+
assert logger.Out_Style._enabled
36+
assert logger.Err_Style._enabled
37+
38+
39+
class TestUtilityFunctions:
40+
"""Test logger utility functions."""
41+
42+
def test_shorten_string_short(self):
43+
"""Test shorten_string returns string unchanged when under limit."""
44+
result = logger.shorten_string("short", 50)
45+
assert result == "short"
46+
47+
def test_shorten_string_long(self):
48+
"""Test shorten_string truncates long strings correctly."""
49+
long_string = "a" * 100
50+
result = logger.shorten_string(long_string, 50)
51+
assert "...(and" in result
52+
assert "more)" in result
53+
assert len(result) <= 50
54+
55+
def test_shorten_string_bytes(self):
56+
"""Test shorten_string handles bytes input."""
57+
byte_string = b"test" * 50
58+
result = logger.shorten_string(byte_string, 50)
59+
assert "...(and" in result
60+
61+
@patch.dict('os.environ', {'COLUMNS': '120'})
62+
def test_get_console_width_from_env(self):
63+
"""Test get_console_width reads from COLUMNS env var."""
64+
width = logger.get_console_width()
65+
assert width == 120
66+
67+
@patch.dict('os.environ', {}, clear=True)
68+
@patch('os.popen')
69+
def test_get_console_width_from_stty(self, mock_popen):
70+
"""Test get_console_width falls back to stty command."""
71+
mock_popen.return_value.read.return_value = "40 80"
72+
width = logger.get_console_width()
73+
assert width == 80
74+
mock_popen.assert_called_once_with('stty size', 'r')
75+
76+
@patch.dict('os.environ', {}, clear=True)
77+
@patch('os.popen')
78+
def test_get_console_width_default(self, mock_popen):
79+
"""Test get_console_width returns default when stty fails."""
80+
mock_popen.return_value.read.side_effect = Exception("stty failed")
81+
width = logger.get_console_width()
82+
assert width == 100
83+
84+
85+
class TestLevelDifferentiatingFormatter:
86+
"""Test custom log message formatter."""
87+
88+
def test_format_error_level(self):
89+
"""Test formatter adds [ERROR] prefix for ERROR level."""
90+
formatter = logger.LevelDifferentiatingFormatter('%(message)s')
91+
record = logging.LogRecord(
92+
name='test', level=40, pathname='', lineno=0,
93+
msg='test error', args=(), exc_info=None
94+
)
95+
formatted = formatter.format(record)
96+
assert '[ERROR]' in formatted
97+
98+
def test_format_warning_level(self):
99+
"""Test formatter adds [WARNING] prefix for WARNING level."""
100+
formatter = logger.LevelDifferentiatingFormatter('%(message)s')
101+
record = logging.LogRecord(
102+
name='test', level=30, pathname='', lineno=0,
103+
msg='test warning', args=(), exc_info=None
104+
)
105+
formatted = formatter.format(record)
106+
assert '[WARNING]' in formatted
107+
108+
def test_format_info_level(self):
109+
"""Test formatter adds [INFO] prefix for INFO level."""
110+
formatter = logger.LevelDifferentiatingFormatter('%(message)s')
111+
record = logging.LogRecord(
112+
name='test', level=20, pathname='', lineno=0,
113+
msg='test info', args=(), exc_info=None
114+
)
115+
formatted = formatter.format(record)
116+
assert '[INFO]' in formatted
117+
118+
def test_format_debug_level(self):
119+
"""Test formatter adds [DEBUG] prefix for DEBUG level."""
120+
formatter = logger.LevelDifferentiatingFormatter('%(message)s')
121+
record = logging.LogRecord(
122+
name='test', level=10, pathname='', lineno=0,
123+
msg='test debug', args=(), exc_info=None
124+
)
125+
formatted = formatter.format(record)
126+
assert '[DEBUG]' in formatted
127+
128+
129+
class TestShprintErrorHandling:
130+
"""Test shprint error handling and edge cases."""
131+
132+
@patch('pythonforandroid.logger.get_console_width')
133+
def test_shprint_with_filter(self, mock_width):
134+
"""Test shprint filters output with _filter parameter."""
135+
mock_width.return_value = 100
136+
137+
command = MagicMock()
138+
# Create a mock error with required attributes
139+
error = Mock(spec=sh.ErrorReturnCode)
140+
error.stdout = b'line1\nfiltered_line\nline3'
141+
error.stderr = b''
142+
command.side_effect = error
143+
144+
with pytest.raises(TypeError):
145+
logger.shprint(command, _filter='filtered', _tail=10)
146+
147+
@patch('pythonforandroid.logger.get_console_width')
148+
def test_shprint_with_filterout(self, mock_width):
149+
"""Test shprint excludes output with _filterout parameter."""
150+
mock_width.return_value = 100
151+
152+
command = MagicMock()
153+
error = Mock(spec=sh.ErrorReturnCode)
154+
error.stdout = b'keep1\nexclude_line\nkeep2'
155+
error.stderr = b''
156+
command.side_effect = error
157+
158+
with pytest.raises(TypeError):
159+
logger.shprint(command, _filterout='exclude', _tail=10)
160+
161+
@patch('pythonforandroid.logger.get_console_width')
162+
@patch('pythonforandroid.logger.stdout')
163+
@patch.dict('os.environ', {'P4A_FULL_DEBUG': '1'})
164+
def test_shprint_full_debug_mode(self, mock_stdout, mock_width):
165+
"""Test shprint in P4A_FULL_DEBUG mode shows all output."""
166+
mock_width.return_value = 100
167+
168+
command = MagicMock()
169+
command.return_value = iter(['debug line 1\n', 'debug line 2\n'])
170+
171+
logger.shprint(command)
172+
# In full debug mode, output is written directly to stdout
173+
assert mock_stdout.write.called
174+
175+
@patch('pythonforandroid.logger.get_console_width')
176+
@patch.dict('os.environ', {}, clear=True)
177+
def test_shprint_critical_failure_exits(self, mock_width):
178+
"""Test shprint exits on critical command failure."""
179+
mock_width.return_value = 100
180+
181+
command = MagicMock()
182+
183+
# Create a proper exception class that mimics sh.ErrorReturnCode
184+
class MockErrorReturnCode(sh.ErrorReturnCode):
185+
def __init__(self):
186+
self.full_cmd = 'test'
187+
self.stdout = b'output'
188+
self.stderr = b'error'
189+
self.exit_code = 1
190+
191+
error = MockErrorReturnCode()
192+
command.side_effect = error
193+
194+
with patch('pythonforandroid.logger.exit', side_effect=SystemExit) as mock_exit:
195+
with pytest.raises(SystemExit):
196+
logger.shprint(command, _critical=True, _tail=5)
197+
mock_exit.assert_called_once_with(1)
198+
199+
200+
class TestLoggingHelpers:
201+
"""Test logging helper functions."""
202+
203+
@patch('pythonforandroid.logger.logger')
204+
def test_info_main(self, mock_logger):
205+
"""Test info_main logs with bright green formatting."""
206+
logger.info_main('test', 'message')
207+
mock_logger.info.assert_called_once()
208+
# Verify the call contains color codes and text
209+
call_args = mock_logger.info.call_args[0][0]
210+
assert 'test' in call_args
211+
assert 'message' in call_args
212+
213+
@patch('pythonforandroid.logger.info')
214+
def test_info_notify(self, mock_info):
215+
"""Test info_notify logs with blue formatting."""
216+
logger.info_notify('notification')
217+
mock_info.assert_called_once()
218+
call_args = mock_info.call_args[0][0]
219+
assert 'notification' in call_args
220+
221+
6222
class TestShprint(unittest.TestCase):
7223

8224
def test_unicode_encode(self):

0 commit comments

Comments
 (0)