33import codecs
44import shutil
55import unittest
6+ import warnings
7+
8+ import pytest
9+
610import buildozer as buildozer_module
711from buildozer import Buildozer
812from io import StringIO
@@ -183,6 +187,225 @@ def test_p4a_recommended_android_ndk_found(
183187 mock_open .assert_called_once ()
184188
185189
190+ class TestBuildozerHelpers :
191+ """Tests for pure-helper methods on the Buildozer class.
192+
193+ These methods either manipulate strings/lists, validate config, or
194+ compose paths; they perform no subprocess, network, or filesystem I/O
195+ (beyond os.path joins). A non-existent specfile keeps Buildozer.__init__
196+ from invoking check_configuration_tokens during construction so each
197+ test can drive the helper directly with a hand-built config.
198+ """
199+
200+ SPECFILE = '/tmp/buildozer_helpers_does_not_exist.spec'
201+
202+ def _new_buildozer (self ):
203+ buildozer = Buildozer (self .SPECFILE )
204+ config = buildozer .config
205+ config .add_section ('app' )
206+ config .set ('app' , 'title' , 'Test App' )
207+ config .set ('app' , 'source.dir' , '.' )
208+ config .set ('app' , 'package.name' , 'testapp' )
209+ config .set ('app' , 'version' , '0.1' )
210+ return buildozer
211+
212+ def test_check_configuration_tokens_happy_path (self ):
213+ buildozer = self ._new_buildozer ()
214+ buildozer .check_configuration_tokens ()
215+
216+ def test_check_configuration_tokens_collects_all_errors (self ):
217+ buildozer = self ._new_buildozer ()
218+ buildozer .config .set ('app' , 'title' , '' )
219+ buildozer .config .set ('app' , 'source.dir' , '' )
220+ buildozer .config .set ('app' , 'package.name' , '' )
221+ buildozer .config .remove_option ('app' , 'version' )
222+ buildozer .config .set ('app' , 'orientation' , 'sideways' )
223+ with mock .patch ('sys.stdout' , new_callable = StringIO ) as out , \
224+ pytest .raises (SystemExit ) as ctx :
225+ buildozer .check_configuration_tokens ()
226+ assert ctx .value .code == 1
227+ output = out .getvalue ()
228+ assert '"title" is missing' in output
229+ assert '"source.dir" is missing' in output
230+ assert '"package.name" is missing' in output
231+ assert '"version"' in output
232+ assert 'sideways' in output
233+
234+ def test_check_configuration_tokens_package_name_starts_with_digit (self ):
235+ buildozer = self ._new_buildozer ()
236+ buildozer .config .set ('app' , 'package.name' , '1myapp' )
237+ with mock .patch ('sys.stdout' , new_callable = StringIO ) as out , \
238+ pytest .raises (SystemExit ):
239+ buildozer .check_configuration_tokens ()
240+ assert 'may not start with a number' in out .getvalue ()
241+
242+ def test_check_configuration_tokens_version_conflict (self ):
243+ buildozer = self ._new_buildozer ()
244+ buildozer .config .set ('app' , 'version.regex' , r'__version__' )
245+ with mock .patch ('sys.stdout' , new_callable = StringIO ) as out , \
246+ pytest .raises (SystemExit ):
247+ buildozer .check_configuration_tokens ()
248+ assert (
249+ 'Conflict between "version" and "version.regex"' in out .getvalue ()
250+ )
251+
252+ def test_check_configuration_tokens_version_filename_missing (self ):
253+ buildozer = self ._new_buildozer ()
254+ buildozer .config .remove_option ('app' , 'version' )
255+ buildozer .config .set ('app' , 'version.regex' , r'__version__' )
256+ with mock .patch ('sys.stdout' , new_callable = StringIO ) as out , \
257+ pytest .raises (SystemExit ):
258+ buildozer .check_configuration_tokens ()
259+ assert '"version.filename" is missing' in out .getvalue ()
260+
261+ def test_migrate_configuration_tokens_renames_deprecated (self ):
262+ buildozer = self ._new_buildozer ()
263+ buildozer .config .set ('app' , 'android.p4a_dir' , '/some/path' )
264+ buildozer .migrate_configuration_tokens ()
265+ assert not buildozer .config .has_option ('app' , 'android.p4a_dir' )
266+ assert buildozer .config .get ('app' , 'p4a.source_dir' ) == '/some/path'
267+
268+ def test_migrate_configuration_tokens_no_app_section_noop (self ):
269+ buildozer = Buildozer (self .SPECFILE )
270+ buildozer .migrate_configuration_tokens ()
271+
272+ def test_check_garden_requirements_no_warning_when_unset (self ):
273+ buildozer = self ._new_buildozer ()
274+ with warnings .catch_warnings (record = True ) as caught :
275+ warnings .simplefilter ('always' )
276+ buildozer .check_garden_requirements ()
277+ deprecations = [
278+ w for w in caught if issubclass (w .category , DeprecationWarning )]
279+ assert deprecations == []
280+
281+ def test_check_garden_requirements_warns_when_set (self ):
282+ buildozer = self ._new_buildozer ()
283+ buildozer .config .set ('app' , 'garden_requirements' , 'somelib' )
284+ with warnings .catch_warnings (record = True ) as caught :
285+ warnings .simplefilter ('always' )
286+ buildozer .check_garden_requirements ()
287+ deprecations = [
288+ w for w in caught if issubclass (w .category , DeprecationWarning )]
289+ assert len (deprecations ) == 1
290+ assert 'garden_requirements' in str (deprecations [0 ].message )
291+
292+ def test_get_version_explicit (self ):
293+ buildozer = self ._new_buildozer ()
294+ assert buildozer .get_version () == '0.1'
295+
296+ def test_get_version_conflict (self ):
297+ buildozer = self ._new_buildozer ()
298+ buildozer .config .set ('app' , 'version.regex' , r'__version__' )
299+ with pytest .raises (Exception ) as ctx :
300+ buildozer .get_version ()
301+ assert 'conflict' in str (ctx .value )
302+
303+ def test_get_version_regex_without_filename (self ):
304+ buildozer = self ._new_buildozer ()
305+ buildozer .config .remove_option ('app' , 'version' )
306+ buildozer .config .set ('app' , 'version.regex' , r'__version__' )
307+ with pytest .raises (Exception ) as ctx :
308+ buildozer .get_version ()
309+ assert 'version.filename is missing' in str (ctx .value )
310+
311+ def test_get_version_filename_without_regex (self ):
312+ buildozer = self ._new_buildozer ()
313+ buildozer .config .remove_option ('app' , 'version' )
314+ buildozer .config .set ('app' , 'version.filename' , '/no/such/file' )
315+ with pytest .raises (Exception ) as ctx :
316+ buildozer .get_version ()
317+ assert 'version.regex is missing' in str (ctx .value )
318+
319+ def test_get_version_from_regex (self ):
320+ buildozer = self ._new_buildozer ()
321+ buildozer .config .remove_option ('app' , 'version' )
322+ buildozer .config .set ('app' , 'version.filename' , 'fakefile.py' )
323+ buildozer .config .set ('app' , 'version.regex' , r'__version__ = "(.+)"' )
324+ with mock .patch (
325+ 'builtins.open' ,
326+ mock .mock_open (read_data = '__version__ = "1.2.3"' )):
327+ assert buildozer .get_version () == '1.2.3'
328+
329+ def test_get_version_regex_no_match (self ):
330+ buildozer = self ._new_buildozer ()
331+ buildozer .config .remove_option ('app' , 'version' )
332+ buildozer .config .set ('app' , 'version.filename' , 'fakefile.py' )
333+ buildozer .config .set ('app' , 'version.regex' , r'__version__ = "(.+)"' )
334+ with mock .patch (
335+ 'builtins.open' ,
336+ mock .mock_open (read_data = 'nothing relevant here' )), \
337+ pytest .raises (Exception ) as ctx :
338+ buildozer .get_version ()
339+ assert 'Unable to find capture version' in str (ctx .value )
340+
341+ def test_get_version_nothing_set (self ):
342+ buildozer = self ._new_buildozer ()
343+ buildozer .config .remove_option ('app' , 'version' )
344+ with pytest .raises (Exception ) as ctx :
345+ buildozer .get_version ()
346+ assert 'Missing version or version.regex' in str (ctx .value )
347+
348+ def test_namify (self ):
349+ buildozer = self ._new_buildozer ()
350+ assert buildozer .namify ('Hello, World!' ) == 'Hello__World_'
351+ assert buildozer .namify ('valid-name_123' ) == 'valid-name_123'
352+
353+ def test_user_build_dir_unset (self ):
354+ buildozer = self ._new_buildozer ()
355+ assert buildozer .user_build_dir is None
356+
357+ def test_user_build_dir_from_legacy_builddir (self ):
358+ buildozer = self ._new_buildozer ()
359+ buildozer .config .add_section ('buildozer' )
360+ buildozer .config .set ('buildozer' , 'builddir' , 'mybuild' )
361+ expected = os .path .realpath (os .path .join (
362+ buildozer .root_dir , 'mybuild' , '.buildozer' ))
363+ assert buildozer .user_build_dir == expected
364+
365+ def test_user_build_dir_from_modern_build_dir (self ):
366+ buildozer = self ._new_buildozer ()
367+ buildozer .config .add_section ('buildozer' )
368+ buildozer .config .set ('buildozer' , 'build_dir' , 'myroot' )
369+ expected = os .path .realpath (
370+ os .path .join (buildozer .root_dir , 'myroot' ))
371+ assert buildozer .user_build_dir == expected
372+
373+ def test_buildozer_dir_default (self ):
374+ buildozer = self ._new_buildozer ()
375+ assert buildozer .buildozer_dir == os .path .join (
376+ buildozer .root_dir , '.buildozer' )
377+
378+ def test_buildozer_dir_uses_user_build_dir (self ):
379+ buildozer = self ._new_buildozer ()
380+ buildozer .config .add_section ('buildozer' )
381+ buildozer .config .set ('buildozer' , 'build_dir' , 'myroot' )
382+ assert buildozer .buildozer_dir == buildozer .user_build_dir
383+
384+ def test_bin_dir_default (self ):
385+ buildozer = self ._new_buildozer ()
386+ assert buildozer .bin_dir == os .path .join (buildozer .root_dir , 'bin' )
387+
388+ def test_bin_dir_user (self ):
389+ buildozer = self ._new_buildozer ()
390+ buildozer .user_bin_dir = '/custom/bin'
391+ assert buildozer .bin_dir == '/custom/bin'
392+
393+ def test_global_packages_dir (self ):
394+ buildozer = self ._new_buildozer ()
395+ buildozer .targetname = 'android'
396+ assert buildozer .global_packages_dir == os .path .join (
397+ os .path .expanduser ('~' ), '.buildozer' , 'android' , 'packages' )
398+
399+ def test_package_full_name_with_domain (self ):
400+ buildozer = self ._new_buildozer ()
401+ buildozer .config .set ('app' , 'package.domain' , 'org.example' )
402+ assert buildozer .package_full_name == 'org.example.testapp'
403+
404+ def test_package_full_name_no_domain (self ):
405+ buildozer = self ._new_buildozer ()
406+ assert buildozer .package_full_name == 'testapp'
407+
408+
186409class TestCopyApplicationSources (unittest .TestCase ):
187410 """Tests for the _copy_application_sources method."""
188411
@@ -333,15 +556,13 @@ def test_ignore_hidden_files_and_directories_in_source(self,
333556 copy_calls = mock_buildops .file_copy .call_args_list
334557 copied_files = [call [0 ][0 ] for call in copy_calls ]
335558
336- self .assertTrue (any ('main.py' in f for f in copied_files ))
337- self .assertTrue (any ('visible' in f and 'code.py' in f
338- for f in copied_files ))
339- self .assertTrue (any ('subdir' in f and 'code2.py' in f
340- for f in copied_files ))
341- self .assertFalse (any ('.hidden' in f for f in copied_files ))
342- self .assertFalse (any ('secret.py' in f for f in copied_files ))
343- self .assertFalse (any ('.hidden_file.py' in f for f in copied_files ))
344- self .assertFalse (any ('.hidden2.py' in f for f in copied_files ))
559+ assert any ('main.py' in f for f in copied_files )
560+ assert any ('visible' in f and 'code.py' in f for f in copied_files )
561+ assert any ('subdir' in f and 'code2.py' in f for f in copied_files )
562+ assert not any ('.hidden' in f for f in copied_files )
563+ assert not any ('secret.py' in f for f in copied_files )
564+ assert not any ('.hidden_file.py' in f for f in copied_files )
565+ assert not any ('.hidden2.py' in f for f in copied_files )
345566
346567 @mock .patch ('buildozer.buildops' )
347568 def test_source_dir_with_hidden_parent (self , mock_buildops ):
@@ -377,10 +598,10 @@ def test_source_dir_with_hidden_parent(self, mock_buildops):
377598 copied_files = [call [0 ][0 ] for call in copy_calls ]
378599
379600 # main.py should be copied (not in a hidden dir relative to source)
380- self . assertTrue ( any ('main.py' in f for f in copied_files ) )
601+ assert any ('main.py' in f for f in copied_files )
381602 # .sub_hidden/secret.py should NOT be copied
382603 # (hidden relative to source)
383- self . assertFalse ( any ('.sub_hidden' in f for f in copied_files ) )
604+ assert not any ('.sub_hidden' in f for f in copied_files )
384605
385606 @mock .patch ('buildozer.buildops' )
386607 def test_include_extensions_filter (self , mock_buildops ):
@@ -400,9 +621,9 @@ def test_include_extensions_filter(self, mock_buildops):
400621 copy_calls = mock_buildops .file_copy .call_args_list
401622 copied_files = [call [0 ][0 ] for call in copy_calls ]
402623
403- self . assertTrue ( any ('main.py' in f for f in copied_files ) )
404- self . assertTrue ( any ('image.png' in f for f in copied_files ) )
405- self . assertFalse ( any ('doc.txt' in f for f in copied_files ) )
624+ assert any ('main.py' in f for f in copied_files )
625+ assert any ('image.png' in f for f in copied_files )
626+ assert not any ('doc.txt' in f for f in copied_files )
406627
407628 @mock .patch ('buildozer.buildops' )
408629 def test_exclude_dirs_filter (self , mock_buildops ):
@@ -423,11 +644,10 @@ def test_exclude_dirs_filter(self, mock_buildops):
423644 copy_calls = mock_buildops .file_copy .call_args_list
424645 copied_files = [call [0 ][0 ] for call in copy_calls ]
425646
426- self .assertTrue (any ('main.py' in f for f in copied_files ))
427- self .assertTrue (any ('src' in f and 'code.py' in f
428- for f in copied_files ))
429- self .assertFalse (any ('tests' in f for f in copied_files ))
430- self .assertFalse (any ('venv' in f for f in copied_files ))
647+ assert any ('main.py' in f for f in copied_files )
648+ assert any ('src' in f and 'code.py' in f for f in copied_files )
649+ assert not any ('tests' in f for f in copied_files )
650+ assert not any ('venv' in f for f in copied_files )
431651
432652 @mock .patch ('buildozer.buildops' )
433653 def test_exclude_patterns (self , mock_buildops ):
@@ -448,7 +668,7 @@ def test_exclude_patterns(self, mock_buildops):
448668 copy_calls = mock_buildops .file_copy .call_args_list
449669 copied_files = [call [0 ][0 ] for call in copy_calls ]
450670
451- self . assertTrue ( any ('main.py' in f for f in copied_files ) )
452- self . assertTrue ( any ('icon.png' in f for f in copied_files ) )
453- self . assertFalse ( any ('LICENSE' in f for f in copied_files ) )
454- self . assertFalse ( any ('photo.jpg' in f for f in copied_files ) )
671+ assert any ('main.py' in f for f in copied_files )
672+ assert any ('icon.png' in f for f in copied_files )
673+ assert not any ('LICENSE' in f for f in copied_files )
674+ assert not any ('photo.jpg' in f for f in copied_files )
0 commit comments