diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-05 14:14:55 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-08-05 14:14:55 (GMT) |
commit | ab7f05de4862f2eec7b77001b34f5fb78d371298 (patch) | |
tree | 0626a8e98410d7519197a254824dd8943ddba61d | |
parent | 3f4d54c5bbd6acb49ed5b46f95db632be0c73fb7 (diff) |
Invalidate cached solutions after changing stability preferences
-rw-r--r-- | TODO | 9 | ||||
-rw-r--r-- | sugar_network/client/injector.py | 23 | ||||
-rw-r--r-- | sugar_network/client/solver.py | 35 | ||||
-rw-r--r-- | tests/__init__.py | 7 | ||||
-rwxr-xr-x | tests/units/client/injector.py | 151 | ||||
-rwxr-xr-x | tests/units/client/solver.py | 79 |
6 files changed, 161 insertions, 143 deletions
@@ -1,13 +1,9 @@ -- invalidate solutions cache on config changes (layers ans stabilities might be changed) -- delete outdated impls on PUTing new one - - (!) Editors' workflows: - (?) log all (including editros) posters of particular document to minimize conflicts about why somthing was changed or better, detailed log for every editor's change - Remove temporal security hole with speciying guid in POST, it was added as a fast hack to support offline creation (with later pushing to a node) -- GC implementations cache - get all localized strings from activity.info while populating local contexts - activities migth need MIME registering while checking-in - changed pulls should take into account accept_length @@ -17,9 +13,4 @@ - increase granularity for sync.chunked_encode() - slave._Pooler might leak events if pullers are not in time to call wait() - revert per-document "downloads" property as "launches", a part of unpersonizalied user_stats -- more useful offline workflow - - keep offline Contexts synchronized with online source - - (?) not only new content in offline mode -- handle proxy prpos for prop= requests - reuse "layer" for home volume instead of "clone" and "favorite" -- local preferences regarding feeds diff --git a/sugar_network/client/injector.py b/sugar_network/client/injector.py index cdcdc5e..de1c6d1 100644 --- a/sugar_network/client/injector.py +++ b/sugar_network/client/injector.py @@ -145,7 +145,7 @@ def _clone(context): shutil.rmtree(cloned.pop(), ignore_errors=True) raise - _set_cached_solution(context, solution) + _set_cached_solution(context, None, solution) def _clone_impl(context_guid, params): @@ -163,7 +163,7 @@ def _clone_impl(context_guid, params): _logger.info('Clone implementation to %r', dst_path) toolkit.cptree(src_path, dst_path) - _set_cached_solution(context_guid, [{ + _set_cached_solution(context_guid, None, [{ 'id': dst_path, 'context': context_guid, 'version': impl['version'], @@ -177,8 +177,9 @@ def _clone_impl(context_guid, params): def _solve(context): pipe.trace('Start solving %s feed', context) + stability = client.stability(context) - solution, stale = _get_cached_solution(context) + solution, stale = _get_cached_solution(context, stability) if stale is False: pipe.trace('Reuse cached solution') return solution @@ -190,8 +191,8 @@ def _solve(context): from sugar_network.client import solver - solution = solver.solve(conn, context) - _set_cached_solution(context, solution) + solution = solver.solve(conn, context, stability) + _set_cached_solution(context, stability, solution) return solution @@ -225,19 +226,21 @@ def _cached_solution_path(guid): return client.path('cache', 'solutions', guid[:2], guid) -def _get_cached_solution(guid): +def _get_cached_solution(guid, stability): path = _cached_solution_path(guid) solution = None if exists(path): try: with file(path) as f: - api_url, solution = json.load(f) + cached_api_url, cached_stability, solution = json.load(f) except Exception, error: _logger.debug('Cannot open %r solution: %s', path, error) if solution is None: return None, None - stale = (api_url != client.api_url.value) + stale = (cached_api_url != client.api_url.value) + if not stale and cached_stability is not None: + stale = set(cached_stability) != set(stability) if not stale and _mtime is not None: stale = (_mtime > os.stat(path).st_mtime) if not stale and _pms_path is not None: @@ -256,9 +259,9 @@ def _get_cached_solution(guid): return solution, stale -def _set_cached_solution(guid, solution): +def _set_cached_solution(guid, stability, solution): path = _cached_solution_path(guid) if not exists(dirname(path)): os.makedirs(dirname(path)) with file(path, 'w') as f: - json.dump([client.api_url.value, solution], f) + json.dump([client.api_url.value, stability, solution], f) diff --git a/sugar_network/client/solver.py b/sugar_network/client/solver.py index 3d18cef..584d0c5 100644 --- a/sugar_network/client/solver.py +++ b/sugar_network/client/solver.py @@ -19,7 +19,6 @@ import sys import logging from os.path import isabs, join, dirname -from sugar_network import client from sugar_network.client import packagekit, SUGAR_API_COMPATIBILITY from sugar_network.toolkit.spec import parse_version from sugar_network.toolkit import http, lsb_release, pipe, exception @@ -34,16 +33,14 @@ from zeroinstall.injector.arch import machine_ranks from zeroinstall.injector.distro import try_cleanup_distro_version -def _interface_init(self, url): - self.uri = url - self.reset() - - -model.Interface.__init__ = _interface_init -reader.check_readable = lambda * args, ** kwargs: True -reader.update_from_cache = lambda * args, ** kwargs: None +model.Interface.__init__ = lambda *args: _interface_init(*args) +reader.check_readable = lambda *args, **kwargs: True +reader.update_from_cache = lambda *args, **kwargs: None +reader.load_feed_from_cache = lambda url, **kwargs: _load_feed(url) _logger = logging.getLogger('zeroinstall') +_stability = None +_conn = None def canonicalize_machine(arch): @@ -70,9 +67,11 @@ def select_architecture(arches): return result_arch -def solve(conn, context): - reader.load_feed_from_cache = lambda url, *args, **kwargs: \ - _load_feed(conn, url) +def solve(conn, context, stability): + global _conn, _stability + + _conn = conn + _stability = stability req = Requirements(context) # TODO @@ -156,6 +155,11 @@ def solve(conn, context): return solution +def _interface_init(self, url): + self.uri = url + self.reset() + + def _impl_new(config, iface, sel): feed = config.iface_cache.get_feed(iface) impl = {'id': sel.id, @@ -186,7 +190,7 @@ def _impl_new(config, iface, sel): return impl -def _load_feed(conn, context): +def _load_feed(context): feed = _Feed(context) if context == 'sugar': @@ -203,9 +207,8 @@ def _load_feed(conn, context): feed_content = None try: - feed_content = conn.get(['context', context], cmd='feed', - stability=client.stability(context), - distro=lsb_release.distributor_id()) + feed_content = _conn.get(['context', context], cmd='feed', + stability=_stability, distro=lsb_release.distributor_id()) pipe.trace('Found %s feed: %r', context, feed_content) except http.ServiceUnavailable: pipe.trace('Failed to fetch %s feed', context) diff --git a/tests/__init__.py b/tests/__init__.py index c4d494d..fcd90a6 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -74,11 +74,10 @@ class Test(unittest.TestCase): shutil.copy(join(root, 'data', 'owner.key.pub'), profile_dir) adapters.DEFAULT_RETRIES = 5 - Option.unsorted_items = [] Option.items = {} - Option.sections = {} Option.config_files = [] - Option._config = None + Option.config = None + Option._parser = None Option._config_to_save = None db.index_flush_timeout.value = 0 db.index_flush_threshold.value = 1 @@ -113,6 +112,8 @@ class Test(unittest.TestCase): injector._pms_path = None journal._ds_root = tmpdir + '/datastore' solver.nodeps = False + solver._stability = None + solver._conn = None downloads._POOL_SIZE = 256 pipe._pipe = None pipe._trace = None diff --git a/tests/units/client/injector.py b/tests/units/client/injector.py index 1010343..7a17748 100755 --- a/tests/units/client/injector.py +++ b/tests/units/client/injector.py @@ -21,6 +21,7 @@ from sugar_network.model.user import User from sugar_network.model.context import Context from sugar_network.model.implementation import Implementation from sugar_network.client import IPCConnection, packagekit, injector, clones, solver +from sugar_network.toolkit import Option from sugar_network import client @@ -153,7 +154,7 @@ Can't find all required implementations: [i for i in pipe]) assert exists('cache/implementation/%s' % impl) assert exists('Activities/topdir/probe') - __, (solution,) = json.load(file('cache/solutions/%s/%s' % (context[:2], context))) + __, __, (solution,) = json.load(file('cache/solutions/%s/%s' % (context[:2], context))) self.assertEqual(tests.tmpdir + '/Activities/topdir', solution['path']) self.assertEqual('probe', file('Activities/topdir/probe').read()) @@ -193,7 +194,7 @@ Can't find all required implementations: for event in injector.clone(context): pass self.assertEqual('exit', event['state']) - __, (solution,) = json.load(file('cache/solutions/%s/%s' % (context[:2], context))) + __, __, (solution,) = json.load(file('cache/solutions/%s/%s' % (context[:2], context))) self.assertEqual(tests.tmpdir + '/Activities/topdir', solution['path']) def test_launch_Online(self): @@ -419,7 +420,7 @@ Can't find all required implementations: self.override(solver, 'solve', lambda *args: solution) self.assertEqual(solution, injector._solve('context')) - self.assertEqual([client.api_url.value, solution], json.load(file('cache/solutions/co/context'))) + self.assertEqual([client.api_url.value, ['stable'], solution], json.load(file('cache/solutions/co/context'))) def test_SolutionsCache_InvalidateByAPIUrl(self): solution = [{'name': 'name', 'context': 'context', 'id': 'id', 'version': 'version'}] @@ -428,13 +429,13 @@ Can't find all required implementations: cached_path = 'cache/solutions/co/context' solution2 = [{'name': 'name2', 'context': 'context2', 'id': 'id2', 'version': 'version2'}] - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) client.api_url.value = 'fake' self.assertEqual(solution, injector._solve('context')) - self.assertEqual(['fake', solution], json.load(file(cached_path))) + self.assertEqual(['fake', ['stable'], solution], json.load(file(cached_path))) def test_SolutionsCache_InvalidateByMtime(self): solution = [{'name': 'name', 'context': 'context', 'id': 'id', 'version': 'version'}] @@ -444,18 +445,18 @@ Can't find all required implementations: solution2 = [{'name': 'name2', 'context': 'context2', 'id': 'id2', 'version': 'version2'}] injector.invalidate_solutions(1) - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) os.utime(cached_path, (1, 1)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) os.utime(cached_path, (2, 2)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) injector.invalidate_solutions(3) self.assertEqual(solution, injector._solve('context')) - self.assertEqual([client.api_url.value, solution], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution], json.load(file(cached_path))) def test_SolutionsCache_InvalidateByPMSMtime(self): solution = [{'name': 'name', 'context': 'context', 'id': 'id', 'version': 'version'}] @@ -467,18 +468,18 @@ Can't find all required implementations: self.touch('pms') os.utime('pms', (1, 1)) solution2 = [{'name': 'name2', 'context': 'context2', 'id': 'id2', 'version': 'version2'}] - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) os.utime(cached_path, (1, 1)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) os.utime(cached_path, (2, 2)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) os.utime('pms', (3, 3)) self.assertEqual(solution, injector._solve('context')) - self.assertEqual([client.api_url.value, solution], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution], json.load(file(cached_path))) def test_SolutionsCache_DeliberateReuseInOffline(self): solution1 = [{'name': 'name', 'context': 'context', 'id': 'id', 'version': 'version'}] @@ -487,13 +488,13 @@ Can't find all required implementations: cached_path = 'cache/solutions/co/context' self.override(client, 'IPCConnection', lambda: _FakeConnection(True)) - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) os.utime(cached_path, (1, 1)) injector.invalidate_solutions(2) self.assertEqual(solution1, injector._solve('context')) self.override(client, 'IPCConnection', lambda: _FakeConnection(False)) - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) os.utime(cached_path, (1, 1)) injector.invalidate_solutions(2) self.assertEqual(solution2, injector._solve('context')) @@ -508,18 +509,18 @@ Can't find all required implementations: solution2 = [{'spec': 'spec', 'name': 'name2', 'context': 'context2', 'id': 'id2', 'version': 'version2'}] self.touch('spec') os.utime('spec', (1, 1)) - self.touch((cached_path, json.dumps([client.api_url.value, solution2]))) + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) os.utime(cached_path, (1, 1)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) os.utime(cached_path, (2, 2)) self.assertEqual(solution2, injector._solve('context')) - self.assertEqual([client.api_url.value, solution2], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) os.utime('spec', (3, 3)) self.assertEqual(solution, injector._solve('context')) - self.assertEqual([client.api_url.value, solution], json.load(file(cached_path))) + self.assertEqual([client.api_url.value, ['stable'], solution], json.load(file(cached_path))) def test_clone_SetExecPermissionsForActivities(self): self.start_online_client([User, Context, Implementation]) @@ -711,7 +712,7 @@ Can't find all required implementations: {'version': '1', 'id': 'dep3', 'context': 'dep3', 'name': 'title3', 'stability': 'packaged'}, {'name': 'title', 'version': '1', 'command': ['echo'], 'context': context, 'id': impl, 'stability': 'stable'}, ]), - sorted(solver.solve(conn, context))) + sorted(solver.solve(conn, context, ['stable']))) def test_LoadFeed_SetPackages(self): self.start_online_client([User, Context, Implementation]) @@ -756,7 +757,7 @@ Can't find all required implementations: return dict([(i, {'name': i, 'pk_id': i, 'version': '1', 'arch': '*', 'installed': True}) for i in names]) self.override(packagekit, 'resolve', resolve) - self.assertRaises(RuntimeError, solver.solve, conn, context) + self.assertRaises(RuntimeError, solver.solve, conn, context, ['stable']) conn.put(['context', 'dep', 'aliases'], { lsb_release.distributor_id(): { @@ -764,7 +765,7 @@ Can't find all required implementations: 'binary': [['bin']], }, }) - self.assertEqual('dep', solver.solve(conn, context)[-1]['context']) + self.assertEqual('dep', solver.solve(conn, context, ['stable'])[-1]['context']) conn.put(['context', 'dep', 'aliases'], { 'foo': { @@ -772,14 +773,14 @@ Can't find all required implementations: 'binary': [['bin']], }, }) - self.assertRaises(RuntimeError, solver.solve, conn, context) + self.assertRaises(RuntimeError, solver.solve, conn, context, ['stable']) conn.put(['context', 'dep', 'aliases'], { lsb_release.distributor_id(): { 'binary': [['bin']], }, }) - self.assertEqual('dep', solver.solve(conn, context)[-1]['context']) + self.assertEqual('dep', solver.solve(conn, context, ['stable'])[-1]['context']) def test_SolveSugar(self): self.touch(('__init__.py', '')) @@ -829,7 +830,7 @@ Can't find all required implementations: {'name': 'title', 'version': '1', 'command': ['echo'], 'context': context, 'id': impl, 'stability': 'stable'}, {'name': 'sugar', 'version': '0.94', 'context': 'sugar', 'path': '/', 'id': 'sugar-0.94', 'stability': 'packaged'}, ], - solver.solve(conn, context)) + solver.solve(conn, context, ['stable'])) self.node_volume['implementation'].update(impl, {'data': { 'spec': { @@ -849,7 +850,7 @@ Can't find all required implementations: {'name': 'title', 'version': '1', 'command': ['echo'], 'context': context, 'id': impl, 'stability': 'stable'}, {'name': 'sugar', 'version': '0.86', 'context': 'sugar', 'path': '/', 'id': 'sugar-0.86', 'stability': 'packaged'}, ], - solver.solve(conn, context)) + solver.solve(conn, context, ['stable'])) def test_StripSugarVersion(self): self.touch(('__init__.py', '')) @@ -899,7 +900,7 @@ Can't find all required implementations: {'name': 'title', 'version': '1', 'command': ['echo'], 'context': context, 'id': impl, 'stability': 'stable'}, {'name': 'sugar', 'version': '0.94', 'context': 'sugar', 'path': '/', 'id': 'sugar-0.94', 'stability': 'packaged'}, ], - solver.solve(conn, context)) + solver.solve(conn, context, ['stable'])) def test_PopupServiceUnavailableInOffline(self): self.touch(('Activities/Activity/activity/activity.info', [ @@ -926,6 +927,100 @@ Can't find all required implementations: ], [i for i in injector.make('context')]) + def test_StabilityPreferences(self): + self.start_online_client() + ipc = IPCConnection() + data = {'spec': {'*-*': {'commands': {'activity': {'exec': 'echo'}}, 'extract': 'topdir'}}} + + context = ipc.post(['context'], { + 'type': 'activity', + 'title': 'title', + 'summary': 'summary', + 'description': 'description', + }) + impl1 = ipc.post(['implementation'], { + 'context': context, + 'license': 'GPLv3+', + 'version': '1', + 'stability': 'stable', + 'notes': '', + }) + self.node_volume['implementation'].update(impl1, {'data': data}) + impl2 = ipc.post(['implementation'], { + 'context': context, + 'license': 'GPLv3+', + 'version': '2', + 'stability': 'testing', + 'notes': '', + }) + self.node_volume['implementation'].update(impl2, {'data': data}) + impl3 = ipc.post(['implementation'], { + 'context': context, + 'license': 'GPLv3+', + 'version': '3', + 'stability': 'buggy', + 'notes': '', + }) + self.node_volume['implementation'].update(impl3, {'data': data}) + impl4 = ipc.post(['implementation'], { + 'context': context, + 'license': 'GPLv3+', + 'version': '4', + 'stability': 'insecure', + 'notes': '', + }) + self.node_volume['implementation'].update(impl4, {'data': data}) + + self.assertEqual('1', injector._solve(context)[0]['version']) + + self.touch(('config', [ + '[stabilities]', + '%s = testing' % context, + ])) + Option.load(['config']) + self.assertEqual('2', injector._solve(context)[0]['version']) + + self.touch(('config', [ + '[stabilities]', + '%s = testing buggy' % context, + ])) + Option.load(['config']) + self.assertEqual('3', injector._solve(context)[0]['version']) + + self.touch(('config', [ + '[stabilities]', + 'default = insecure', + '%s = stable' % context, + ])) + Option.load(['config']) + self.assertEqual('1', injector._solve(context)[0]['version']) + + self.touch(('config', [ + '[stabilities]', + 'default = insecure', + ])) + Option.load(['config']) + self.assertEqual('4', injector._solve(context)[0]['version']) + + def test_SolutionsCache_InvalidateByStabilityPreferences(self): + solution = [{'name': 'name', 'context': 'context', 'id': 'id', 'version': 'version'}] + self.override(client, 'IPCConnection', lambda: _FakeConnection(True)) + self.override(solver, 'solve', lambda *args: solution) + cached_path = 'cache/solutions/co/context' + + solution2 = [{'name': 'name2', 'context': 'context2', 'id': 'id2', 'version': 'version2'}] + self.touch((cached_path, json.dumps([client.api_url.value, ['stable'], solution2]))) + self.assertEqual(solution2, injector._solve('context')) + self.assertEqual([client.api_url.value, ['stable'], solution2], json.load(file(cached_path))) + + self.touch(('config', [ + '[stabilities]', + 'context = buggy', + ])) + Option.load(['config']) + self.assertEqual(solution, injector._solve('context')) + self.assertEqual([client.api_url.value, ['buggy'], solution], json.load(file(cached_path))) + class _FakeConnection(object): diff --git a/tests/units/client/solver.py b/tests/units/client/solver.py index 84d5456..6e35a50 100755 --- a/tests/units/client/solver.py +++ b/tests/units/client/solver.py @@ -6,7 +6,7 @@ import os from __init__ import tests from sugar_network.client import IPCConnection, packagekit, solver, clones -from sugar_network.toolkit import lsb_release, Option +from sugar_network.toolkit import lsb_release class SolverTest(tests.Test): @@ -64,7 +64,7 @@ class SolverTest(tests.Test): }, }) - solution = solver.solve(ipc, 'bundle_id') + solution = solver.solve(ipc, 'bundle_id', ['stable']) self.assertEqual( 2, len(solution)) self.assertEqual( @@ -74,81 +74,6 @@ class SolverTest(tests.Test): ('dep', '0'), (solution[1]['context'], solution[1]['version'])) - def test_StabilityPreferences(self): - self.start_online_client() - ipc = IPCConnection() - data = {'spec': {'*-*': {'commands': {'activity': {'exec': 'echo'}}, 'extract': 'topdir'}}} - - context = ipc.post(['context'], { - 'type': 'activity', - 'title': 'title', - 'summary': 'summary', - 'description': 'description', - }) - impl1 = ipc.post(['implementation'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '1', - 'stability': 'stable', - 'notes': '', - }) - self.node_volume['implementation'].update(impl1, {'data': data}) - impl2 = ipc.post(['implementation'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '2', - 'stability': 'testing', - 'notes': '', - }) - self.node_volume['implementation'].update(impl2, {'data': data}) - impl3 = ipc.post(['implementation'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '3', - 'stability': 'buggy', - 'notes': '', - }) - self.node_volume['implementation'].update(impl3, {'data': data}) - impl4 = ipc.post(['implementation'], { - 'context': context, - 'license': 'GPLv3+', - 'version': '4', - 'stability': 'insecure', - 'notes': '', - }) - self.node_volume['implementation'].update(impl4, {'data': data}) - - self.assertEqual('1', solver.solve(ipc, context)[0]['version']) - - self.touch(('config', [ - '[stabilities]', - '%s = testing' % context, - ])) - Option.load(['config']) - self.assertEqual('2', solver.solve(ipc, context)[0]['version']) - - self.touch(('config', [ - '[stabilities]', - '%s = testing buggy' % context, - ])) - Option.load(['config']) - self.assertEqual('3', solver.solve(ipc, context)[0]['version']) - - self.touch(('config', [ - '[stabilities]', - 'default = insecure', - '%s = stable' % context, - ])) - Option.load(['config']) - self.assertEqual('1', solver.solve(ipc, context)[0]['version']) - - self.touch(('config', [ - '[stabilities]', - 'default = insecure', - ])) - Option.load(['config']) - self.assertEqual('4', solver.solve(ipc, context)[0]['version']) - if __name__ == '__main__': tests.main() |