Skip to content

Commit c0e1d71

Browse files
authored
Merge pull request #3255 from AndreMiras/feature/increase_test_coverage
✅ Increase test coverage for logger, prerequisites, and pythonpackage
2 parents 0155c7e + 62e8089 commit c0e1d71

File tree

3 files changed

+613
-1
lines changed

3 files changed

+613
-1
lines changed

tests/test_logger.py

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

8228
def test_unicode_encode(self):

0 commit comments

Comments
 (0)