Skip to content

Commit 406a2ba

Browse files
committed
✅ Cover Target base class and Buildozer helpers
Also extract _check_package_domain helper from cmd_release to dedupe the near-identical org.test and org.kivy guard blocks, preserving the user-visible output byte-for-byte.
1 parent ed59d36 commit 406a2ba

3 files changed

Lines changed: 708 additions & 54 deletions

File tree

buildozer/target.py

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -107,39 +107,47 @@ def cmd_debug(self, *args):
107107
self.build_mode = 'debug'
108108
self.buildozer.build()
109109

110-
def cmd_release(self, *args):
110+
def _check_package_domain(self, domain, override_env_var, body_lines):
111+
if self.buildozer.config.get("app", "package.domain") != domain:
112+
return
111113
error = self.logger.error
112-
self.buildozer.prepare_for_build()
113-
if self.buildozer.config.get("app", "package.domain") == "org.test":
114-
error("")
115-
error("ERROR: Trying to release a package that starts with org.test")
116-
error("")
117-
error("The package.domain org.test is, as the name intended, a test.")
118-
error("Once you published an application with org.test,")
119-
error("you cannot change it, it will be part of the identifier")
120-
error("for Google Play / App Store / etc.")
121-
error("")
122-
error("So change package.domain to anything else.")
123-
error("")
124-
error("If you messed up before, set the environment variable to force the build:")
125-
error("export BUILDOZER_ALLOW_ORG_TEST_DOMAIN=1")
126-
error("")
127-
if "BUILDOZER_ALLOW_ORG_TEST_DOMAIN" not in os.environ:
128-
exit(1)
129-
130-
if self.buildozer.config.get("app", "package.domain") == "org.kivy":
131-
error("")
132-
error("ERROR: Trying to release a package that starts with org.kivy")
133-
error("")
134-
error("The package.domain org.kivy is reserved for the Kivy official")
135-
error("applications. Please use your own domain.")
136-
error("")
137-
error("If you are a Kivy developer, add an export in your shell")
138-
error("export BUILDOZER_ALLOW_KIVY_ORG_DOMAIN=1")
139-
error("")
140-
if "BUILDOZER_ALLOW_KIVY_ORG_DOMAIN" not in os.environ:
141-
exit(1)
114+
error("")
115+
error("ERROR: Trying to release a package that starts with {}".format(domain))
116+
error("")
117+
for line in body_lines:
118+
error(line)
119+
error("")
120+
if override_env_var not in os.environ:
121+
exit(1)
142122

123+
def cmd_release(self, *args):
124+
self.buildozer.prepare_for_build()
125+
self._check_package_domain(
126+
"org.test",
127+
"BUILDOZER_ALLOW_ORG_TEST_DOMAIN",
128+
[
129+
"The package.domain org.test is, as the name intended, a test.",
130+
"Once you published an application with org.test,",
131+
"you cannot change it, it will be part of the identifier",
132+
"for Google Play / App Store / etc.",
133+
"",
134+
"So change package.domain to anything else.",
135+
"",
136+
"If you messed up before, set the environment variable to force the build:",
137+
"export BUILDOZER_ALLOW_ORG_TEST_DOMAIN=1",
138+
],
139+
)
140+
self._check_package_domain(
141+
"org.kivy",
142+
"BUILDOZER_ALLOW_KIVY_ORG_DOMAIN",
143+
[
144+
"The package.domain org.kivy is reserved for the Kivy official",
145+
"applications. Please use your own domain.",
146+
"",
147+
"If you are a Kivy developer, add an export in your shell",
148+
"export BUILDOZER_ALLOW_KIVY_ORG_DOMAIN=1",
149+
],
150+
)
143151
self.build_mode = 'release'
144152
self.buildozer.build()
145153

tests/test_buildozer.py

Lines changed: 243 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import codecs
44
import shutil
55
import unittest
6+
import warnings
7+
8+
import pytest
9+
610
import buildozer as buildozer_module
711
from buildozer import Buildozer
812
from 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+
186409
class 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

Comments
 (0)