Skip to content

Commit 168b12d

Browse files
author
Jonas Thiem
committed
Make test_get_bootstraps_from_recipes() deterministic and better
1 parent c8e8cf9 commit 168b12d

File tree

2 files changed

+174
-32
lines changed

2 files changed

+174
-32
lines changed

pythonforandroid/bootstrap.py

Lines changed: 98 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import functools
2+
import glob
3+
import importlib
4+
import os
15
from os.path import (join, dirname, isdir, normpath, splitext, basename)
26
from os import listdir, walk, sep
37
import sh
48
import shlex
5-
import glob
6-
import importlib
7-
import os
89
import shutil
910

1011
from pythonforandroid.logger import (warning, shprint, info, logger,
@@ -138,36 +139,52 @@ def run_distribute(self):
138139
self.distribution.save_info(self.dist_dir)
139140

140141
@classmethod
141-
def list_bootstraps(cls):
142+
def all_bootstraps(cls):
142143
'''Find all the available bootstraps and return them.'''
143144
forbidden_dirs = ('__pycache__', 'common')
144145
bootstraps_dir = join(dirname(__file__), 'bootstraps')
146+
result = set()
145147
for name in listdir(bootstraps_dir):
146148
if name in forbidden_dirs:
147149
continue
148150
filen = join(bootstraps_dir, name)
149151
if isdir(filen):
150-
yield name
152+
result.add(name)
153+
return result
151154

152155
@classmethod
153156
def get_bootstrap_from_recipes(cls, recipes, ctx):
154157
'''Returns a bootstrap whose recipe requirements do not conflict with
155158
the given recipes.'''
156159
info('Trying to find a bootstrap that matches the given recipes.')
157160
bootstraps = [cls.get_bootstrap(name, ctx)
158-
for name in cls.list_bootstraps()]
159-
acceptable_bootstraps = []
161+
for name in cls.all_bootstraps()]
162+
acceptable_bootstraps = set()
163+
known_web_packages = {"flask"} # to pick webview over service_only
164+
default_recipe_priorities = [
165+
"webview", "sdl2", "service_only" # last is highest
166+
]
167+
recipes_with_deps_lists = expand_dependencies(recipes, ctx)
168+
# ^^ NOTE: these are just the default priorities if no special rules
169+
# apply (which you can find in the code below), so basically if no
170+
# known graphical lib or web lib is used - in which case service_only
171+
# is the most reasonable guess.
172+
173+
# Find out which bootstraps are acceptable:
160174
for bs in bootstraps:
161175
if not bs.can_be_chosen_automatically:
162176
continue
163-
possible_dependency_lists = expand_dependencies(bs.recipe_depends)
177+
possible_dependency_lists = expand_dependencies(bs.recipe_depends, ctx)
164178
for possible_dependencies in possible_dependency_lists:
165179
ok = True
180+
# Check if the bootstap's dependencies have an internal conflict:
166181
for recipe in possible_dependencies:
167182
recipe = Recipe.get_recipe(recipe, ctx)
168183
if any([conflict in recipes for conflict in recipe.conflicts]):
169184
ok = False
170185
break
186+
# Check if bootstrap's dependencies conflict with chosen
187+
# packages:
171188
for recipe in recipes:
172189
try:
173190
recipe = Recipe.get_recipe(recipe, ctx)
@@ -180,14 +197,61 @@ def get_bootstrap_from_recipes(cls, recipes, ctx):
180197
ok = False
181198
break
182199
if ok and bs not in acceptable_bootstraps:
183-
acceptable_bootstraps.append(bs)
200+
acceptable_bootstraps.add(bs)
201+
184202
info('Found {} acceptable bootstraps: {}'.format(
185203
len(acceptable_bootstraps),
186204
[bs.name for bs in acceptable_bootstraps]))
187-
if acceptable_bootstraps:
188-
info('Using the first of these: {}'
189-
.format(acceptable_bootstraps[0].name))
190-
return acceptable_bootstraps[0]
205+
206+
def have_dependency_in_recipes(dep):
207+
for dep_list in recipes_with_deps_lists:
208+
if dep in dep_list:
209+
return True
210+
return False
211+
212+
# Special rule: return SDL2 bootstrap if there's an sdl2 dep:
213+
if (have_dependency_in_recipes("sdl2") and
214+
"sdl2" in [b.name for b in acceptable_bootstraps]
215+
):
216+
info('Using sdl2 bootstrap since it is in dependencies')
217+
return cls.get_bootstrap("sdl2", ctx)
218+
219+
# Special rule: return "webview" if we depend on common web recipe:
220+
for possible_web_dep in known_web_packages:
221+
if have_dependency_in_recipes(possible_web_dep):
222+
# We have a web package dep!
223+
if "webview" in [b.name for b in acceptable_bootstraps]:
224+
info('Using webview bootstrap since common web packages '
225+
'were found {}'.format(
226+
known_web_packages.intersection(recipes)
227+
))
228+
return cls.get_bootstrap("webview", ctx)
229+
230+
# Otherwise, go by a default priority ordering to pick best one:
231+
def bootstrap_priority(a, b):
232+
def rank_bootstrap(bootstrap):
233+
""" Returns a ranking index for each bootstrap,
234+
with higher priority ranked with higher number. """
235+
if bootstrap.name in default_recipe_priorities:
236+
return default_recipe_priorities.index(bootstrap.name) + 1
237+
return 0
238+
# Rank bootstraps in order:
239+
rank_a = rank_bootstrap(a)
240+
rank_b = rank_bootstrap(b)
241+
if rank_a != rank_b:
242+
return (rank_b - rank_a)
243+
else:
244+
return (a.name - b.name) # alphabetic sort for determinism
245+
246+
prioritized_acceptable_bootstraps = sorted(
247+
list(acceptable_bootstraps),
248+
key=functools.cmp_to_key(bootstrap_priority)
249+
)
250+
251+
if prioritized_acceptable_bootstraps:
252+
info('Using the highest ranked/first of these: {}'
253+
.format(prioritized_acceptable_bootstraps[0].name))
254+
return prioritized_acceptable_bootstraps[0]
191255
return None
192256

193257
@classmethod
@@ -299,9 +363,26 @@ def fry_eggs(self, sitepackages):
299363
shprint(sh.rm, '-rf', d)
300364

301365

302-
def expand_dependencies(recipes):
366+
def expand_dependencies(recipes, ctx):
367+
""" This function expands to lists of all different available
368+
alternative recipe combinations, with the dependencies added in
369+
ONLY for all the not-with-alternative recipes.
370+
(So this is like the deps graph very simplified and incomplete, but
371+
hopefully good enough for most basic bootstrap compatibility checks)
372+
"""
373+
374+
# Add in all the deps of recipes where there is no alternative:
375+
recipes_with_deps = list(recipes)
376+
for entry in recipes:
377+
if not isinstance(entry, (tuple, list)) or len(entry) == 1:
378+
if isinstance(entry, (tuple, list)):
379+
entry = entry[0]
380+
recipe = Recipe.get_recipe(entry, ctx)
381+
recipes_with_deps += recipe.depends
382+
383+
# Split up lists by available alternatives:
303384
recipe_lists = [[]]
304-
for recipe in recipes:
385+
for recipe in recipes_with_deps:
305386
if isinstance(recipe, (tuple, list)):
306387
new_recipe_lists = []
307388
for alternative in recipe:
@@ -311,6 +392,6 @@ def expand_dependencies(recipes):
311392
new_recipe_lists.append(new_list)
312393
recipe_lists = new_recipe_lists
313394
else:
314-
for old_list in recipe_lists:
315-
old_list.append(recipe)
395+
for existing_list in recipe_lists:
396+
existing_list.append(recipe)
316397
return recipe_lists

tests/test_bootstrap.py

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
12
import os
23
import sh
3-
44
import unittest
55

66
try:
@@ -9,12 +9,14 @@
99
# `Python 2` or lower than `Python 3.3` does not
1010
# have the `unittest.mock` module built-in
1111
import mock
12-
from pythonforandroid.bootstrap import Bootstrap
12+
from pythonforandroid.bootstrap import Bootstrap, expand_dependencies
1313
from pythonforandroid.distribution import Distribution
1414
from pythonforandroid.recipe import Recipe
1515
from pythonforandroid.archs import ArchARMv7_a
1616
from pythonforandroid.build import Context
1717

18+
from test_graph import get_fake_recipe
19+
1820

1921
class BaseClassSetupBootstrap(object):
2022
"""
@@ -100,33 +102,92 @@ def test_build_dist_dirs(self):
100102
bs.get_common_dir().endswith("pythonforandroid/bootstraps/common")
101103
)
102104

103-
def test_list_bootstraps(self):
105+
def test_all_bootstraps(self):
104106
"""A test which will initialize a bootstrap and will check if the
105107
method :meth:`~pythonforandroid.bootstrap.Bootstrap.list_bootstraps`
106108
returns the expected values, which should be: `empty", `service_only`,
107109
`webview` and `sdl2`
108110
"""
109111
expected_bootstraps = {"empty", "service_only", "webview", "sdl2"}
110-
set_of_bootstraps = set(Bootstrap().list_bootstraps())
112+
set_of_bootstraps = Bootstrap().all_bootstraps()
111113
self.assertEqual(
112114
expected_bootstraps, expected_bootstraps & set_of_bootstraps
113115
)
114116
self.assertEqual(len(expected_bootstraps), len(set_of_bootstraps))
115117

118+
def test_expand_dependencies(self):
119+
# Test depenency expansion of a recipe with no alternatives:
120+
expanded_result_1 = expand_dependencies(["pysdl2"], self.ctx)
121+
assert({"sdl2", "pysdl2", "python3"} in
122+
[set(s) for s in expanded_result_1])
123+
124+
# Test all alternatives are listed (they won't have dependencies
125+
# expanded since expand_dependencies() is too simplistic):
126+
expanded_result_2 = expand_dependencies([("pysdl2", "kivy")], self.ctx)
127+
assert([["pysdl2"], ["kivy"]] == expanded_result_2)
128+
116129
def test_get_bootstraps_from_recipes(self):
117-
"""A test which will initialize a bootstrap and will check if the
118-
method :meth:`~pythonforandroid.bootstrap.Bootstrap.
119-
get_bootstraps_from_recipes` returns the expected values
120-
"""
121-
recipes_sdl2 = {"sdl2", "python3", "kivy"}
122-
bs = Bootstrap().get_bootstrap_from_recipes(recipes_sdl2, self.ctx)
130+
import pythonforandroid.recipe
131+
original_get_recipe = pythonforandroid.recipe.Recipe.get_recipe
132+
133+
@mock.patch("pythonforandroid.recipe.Recipe.get_recipe")
134+
def do_test_get_bootstraps_from_recipes(mock_get_recipe):
135+
"""A test which will initialize a bootstrap and will check if the
136+
method :meth:`~pythonforandroid.bootstrap.Bootstrap.
137+
get_bootstraps_from_recipes` returns the expected values
138+
"""
139+
140+
# Use original get_recipe() until we need something else later:
141+
mock_get_recipe.side_effect = original_get_recipe
142+
143+
# Test that SDL2 works with kivy:
144+
recipes_sdl2 = {"sdl2", "python3", "kivy"}
145+
bs = Bootstrap().get_bootstrap_from_recipes(recipes_sdl2, self.ctx)
146+
self.assertEqual(bs.name, "sdl2")
147+
148+
# Test that pysdl2 or kivy alone will also yield SDL2 (dependency):
149+
recipes_pysdl2_only = {"pysdl2"}
150+
bs = Bootstrap().get_bootstrap_from_recipes(recipes_pysdl2_only, self.ctx)
151+
self.assertEqual(bs.name, "sdl2")
152+
recipes_kivy_only = {"kivy"}
153+
bs = Bootstrap().get_bootstrap_from_recipes(recipes_kivy_only, self.ctx)
154+
self.assertEqual(bs.name, "sdl2")
155+
156+
# Test that something conflicting with sdl2 won't give sdl2:
157+
def _add_sdl2_conflicting_recipe(name, ctx):
158+
if name == "conflictswithsdl2":
159+
if name not in pythonforandroid.recipe.Recipe.recipes:
160+
pythonforandroid.recipe.Recipe.recipes[name] = (
161+
get_fake_recipe("sdl2", conflicts=["sdl2"])
162+
)
163+
return original_get_recipe(name, ctx)
164+
mock_get_recipe.side_effect = _add_sdl2_conflicting_recipe
165+
recipes_with_sdl2_conflict = {"python3", "conflictswithsdl2"}
166+
bs = Bootstrap().get_bootstrap_from_recipes(
167+
recipes_with_sdl2_conflict, self.ctx
168+
)
169+
self.assertNotEqual(bs.name, "sdl2")
123170

124-
self.assertEqual(bs.name, "sdl2")
171+
# Test using flask will default to webview:
172+
recipes_with_flask = {"python3", "flask"}
173+
bs = Bootstrap().get_bootstrap_from_recipes(
174+
recipes_with_flask, self.ctx
175+
)
176+
self.assertEqual(bs.name, "webview")
177+
178+
# Test using random packages will default to service_only:
179+
recipes_with_no_sdl2_or_web = {"python3", "numpy"}
180+
bs = Bootstrap().get_bootstrap_from_recipes(
181+
recipes_with_no_sdl2_or_web, self.ctx
182+
)
183+
self.assertEqual(bs.name, "service_only")
184+
185+
# test wrong recipes
186+
wrong_recipes = {"python2", "python3", "pyjnius"}
187+
bs = Bootstrap().get_bootstrap_from_recipes(wrong_recipes, self.ctx)
188+
self.assertIsNone(bs)
125189

126-
# test wrong recipes
127-
wrong_recipes = {"python2", "python3", "pyjnius"}
128-
bs = Bootstrap().get_bootstrap_from_recipes(wrong_recipes, self.ctx)
129-
self.assertIsNone(bs)
190+
do_test_get_bootstraps_from_recipes()
130191

131192
@mock.patch("pythonforandroid.bootstrap.ensure_dir")
132193
def test_prepare_dist_dir(self, mock_ensure_dir):

0 commit comments

Comments
 (0)