def testNoNeedDl(self): driver = Driver(requirements = Requirements(foo_iface_uri), config = self.config) assert driver.need_download() driver = Driver(requirements = Requirements(os.path.abspath('Foo.xml')), config = self.config) assert not driver.need_download() assert driver.solver.ready
def testLocalPath(self): # 0launch --get-selections Local.xml iface = os.path.join(mydir, "Local.xml") driver = Driver(requirements = Requirements(iface), config = self.config) driver.need_download() assert driver.solver.ready s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") # Reload selections and check they're the same root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) local_path = s2.selections[iface].local_path assert os.path.isdir(local_path), local_path assert not s2.selections[iface].digests, s2.selections[iface].digests # Add a newer implementation and try again feed = self.config.iface_cache.get_feed(iface) impl = model.ZeroInstallImplementation(feed, "foo bar=123", local_path = None) impl.version = model.parse_version('1.0') impl.commands["run"] = model.Command(qdom.Element(namespaces.XMLNS_IFACE, 'command', {'path': 'dummy', 'name': 'run'}), None) impl.add_download_source('http://localhost/bar.tgz', 1000, None) feed.implementations = {impl.id: impl} assert driver.need_download() assert driver.solver.ready, driver.solver.get_failure_reason() s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) xml = s2.toDOM().toxml("utf-8") qdom.parse(BytesIO(xml)) assert s2.selections[iface].local_path is None assert not s2.selections[iface].digests, s2.selections[iface].digests assert s2.selections[iface].id == 'foo bar=123'
def testDLfeed(self): self.cache_iface( foo_iface_uri, """<?xml version="1.0" ?> <interface last-modified="1110752708" uri="%s" xmlns="http://zero-install.sourceforge.net/2004/injector/interface"> <name>Foo</name> <summary>Foo</summary> <description>Foo</description> <feed src='http://example.com'/> </interface>""" % foo_iface_uri, ) driver = Driver(requirements=Requirements(foo_iface_uri), config=self.config) self.config.network_use = model.network_full assert driver.need_download() feed = self.config.iface_cache.get_feed(foo_iface_uri) feed.feeds = [model.Feed("/BadFeed", None, False)] logger.setLevel(logging.ERROR) assert driver.need_download() # Triggers warning logger.setLevel(logging.WARN)
def testLocalArchive(self): local_iface = os.path.join(mydir, 'LocalArchive.xml') with open(local_iface, 'rb') as stream: root = qdom.parse(stream) # Not local => error feed = model.ZeroInstallFeed(root) impl = feed.implementations['impl1'] blocker = self.config.fetcher.download_impls([impl], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert "Relative URL 'HelloWorld.tgz' in non-local feed" in str( ex), ex feed = model.ZeroInstallFeed(root, local_path=local_iface) # Missing file impl2 = feed.implementations['impl2'] blocker = self.config.fetcher.download_impls([impl2], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert 'tests/IDONTEXIST.tgz' in str(ex), ex # Wrong size impl3 = feed.implementations['impl3'] blocker = self.config.fetcher.download_impls([impl3], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert 'feed says 177, but actually 176 bytes' in str(ex), ex self.config.network_use = model.network_offline r = Requirements(local_iface) r.command = None driver = Driver(requirements=r, config=self.config) driver.need_download() assert driver.solver.ready, driver.solver.get_failure_reason() # Local => OK impl = feed.implementations['impl1'] path = self.config.stores.lookup_maybe(impl.digests) assert not path blocker = self.config.fetcher.download_impls([impl], self.config.stores) tasks.wait_for_blocker(blocker) path = self.config.stores.lookup_any(impl.digests) assert os.path.exists(os.path.join(path, 'HelloWorld'))
def ensure_cached(uri, command = 'run', config = None): """Ensure that an implementation of uri is cached. If not, it downloads one. It uses the GUI if a display is available, or the console otherwise. @param uri: the required interface @type uri: str @return: the selected implementations, or None if the user cancelled @rtype: L{zeroinstall.injector.selections.Selections} """ from zeroinstall.injector.driver import Driver if config is None: from zeroinstall.injector.config import load_config config = load_config() from zeroinstall.injector.requirements import Requirements requirements = Requirements(uri) requirements.command = command d = Driver(config, requirements) if d.need_download() or not d.solver.ready: sels = get_selections_gui(uri, ['--command', command], use_gui = None) if sels != DontUseGUI: return sels done = d.solve_and_download_impls() tasks.wait_for_blocker(done) return d.solver.selections
def testDistro(self): with output_suppressed(): native_url = 'http://example.com:8000/Native.xml' # Initially, we don't have the feed at all... master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is None, master_feed trust.trust_db.trust_key('DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') run_server('Native.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') driver = Driver(requirements = Requirements(native_url), config = self.config) assert driver.need_download() solve = driver.solve_with_downloads() tasks.wait_for_blocker(solve) tasks.check(solve) master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is not None assert master_feed.implementations == {} distro_feed_url = master_feed.get_distro_feed() assert distro_feed_url is not None distro_feed = self.config.iface_cache.get_feed(distro_feed_url) assert distro_feed is not None assert len(distro_feed.implementations) == 2, distro_feed.implementations
def testDistro(self): with output_suppressed(): native_url = 'http://example.com:8000/Native.xml' # Initially, we don't have the feed at all... master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is None, master_feed trust.trust_db.trust_key( 'DE937DD411906ACF7C263B396FCF121BE2390E0B', 'example.com:8000') run_server( 'Native.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') driver = Driver(requirements=Requirements(native_url), config=self.config) assert driver.need_download() solve = driver.solve_with_downloads() tasks.wait_for_blocker(solve) tasks.check(solve) master_feed = self.config.iface_cache.get_feed(native_url) assert master_feed is not None assert master_feed.implementations == {} distro_feed_url = master_feed.get_distro_feed() assert distro_feed_url is not None distro_feed = self.config.iface_cache.get_feed(distro_feed_url) assert distro_feed is not None assert len( distro_feed.implementations) == 2, distro_feed.implementations
def testCommands(self): iface = os.path.join(mydir, "Command.xml") driver = Driver(requirements=Requirements(iface), config=self.config) driver.need_download() assert driver.solver.ready impl = driver.solver.selections[self.config.iface_cache.get_interface( iface)] assert impl.id == 'c' assert impl.main == 'test-gui' dep_impl_uri = impl.commands['run'].requires[0].interface dep_impl = driver.solver.selections[ self.config.iface_cache.get_interface(dep_impl_uri)] assert dep_impl.id == 'sha1=256' s1 = driver.solver.selections assert s1.commands[0].path == 'test-gui' xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) assert s2.commands[0].path == 'test-gui' impl = s2.selections[iface] assert impl.id == 'c' assert s2.commands[0].qdom.attrs['http://custom attr'] == 'namespaced' custom_element = s2.commands[0].qdom.childNodes[0] assert custom_element.name == 'child' dep_impl = s2.selections[dep_impl_uri] assert dep_impl.id == 'sha1=256' d = Driver(self.config, requirements.Requirements(runexec)) need_download = d.need_download() assert need_download == False xml = d.solver.selections.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s3 = selections.Selections(root) runnable_impl = s3.selections[runnable] assert 'foo' in runnable_impl.commands assert 'run' in runnable_impl.commands
def testCommands(self): iface = os.path.join(mydir, "Command.xml") driver = Driver(requirements = Requirements(iface), config = self.config) driver.need_download() assert driver.solver.ready impl = driver.solver.selections[self.config.iface_cache.get_interface(iface)] assert impl.id == 'c' assert impl.main == 'test-gui' dep_impl_uri = impl.commands['run'].requires[0].interface dep_impl = driver.solver.selections[self.config.iface_cache.get_interface(dep_impl_uri)] assert dep_impl.id == 'sha1=256' s1 = driver.solver.selections assert s1.commands[0].path == 'test-gui' xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) assert s2.commands[0].path == 'test-gui' impl = s2.selections[iface] assert impl.id == 'c' assert s2.commands[0].qdom.attrs['http://custom attr'] == 'namespaced' custom_element = s2.commands[0].qdom.childNodes[0] assert custom_element.name == 'child' dep_impl = s2.selections[dep_impl_uri] assert dep_impl.id == 'sha1=256' d = Driver(self.config, requirements.Requirements(runexec)) need_download = d.need_download() assert need_download == False xml = d.solver.selections.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s3 = selections.Selections(root) runnable_impl = s3.selections[runnable] assert 'foo' in runnable_impl.commands assert 'run' in runnable_impl.commands
def testAcceptKey(self): with output_suppressed(): run_server('Hello', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') driver = Driver(requirements = Requirements('http://localhost:8000/Hello'), config = self.config) assert driver.need_download() sys.stdin = Reply("Y\n") try: download_and_execute(driver, ['Hello'], main = 'Missing') assert 0 except model.SafeException as ex: if "HelloWorld/Missing" not in str(ex): raise
def testLocalPath(self): # 0launch --get-selections Local.xml iface = os.path.join(mydir, "Local.xml") driver = Driver(requirements=Requirements(iface), config=self.config) driver.need_download() assert driver.solver.ready s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") # Reload selections and check they're the same root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) local_path = s2.selections[iface].local_path assert os.path.isdir(local_path), local_path assert not s2.selections[iface].digests, s2.selections[iface].digests # Add a newer implementation and try again feed = self.config.iface_cache.get_feed(iface) impl = model.ZeroInstallImplementation(feed, "foo bar=123", local_path=None) impl.version = model.parse_version('1.0') impl.commands["run"] = model.Command( qdom.Element(namespaces.XMLNS_IFACE, 'command', { 'path': 'dummy', 'name': 'run' }), None) impl.add_download_source('http://localhost/bar.tgz', 1000, None) feed.implementations = {impl.id: impl} assert driver.need_download() assert driver.solver.ready, driver.solver.get_failure_reason() s1 = driver.solver.selections xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) s2 = selections.Selections(root) xml = s2.toDOM().toxml("utf-8") qdom.parse(BytesIO(xml)) assert s2.selections[iface].local_path is None assert not s2.selections[iface].digests, s2.selections[iface].digests assert s2.selections[iface].id == 'foo bar=123'
def testDLfeed(self): self.cache_iface(foo_iface_uri, """<?xml version="1.0" ?> <interface last-modified="1110752708" uri="%s" xmlns="http://zero-install.sourceforge.net/2004/injector/interface"> <name>Foo</name> <summary>Foo</summary> <description>Foo</description> <feed src='http://example.com'/> </interface>""" % foo_iface_uri) driver = Driver(requirements = Requirements(foo_iface_uri), config = self.config) self.config.network_use = model.network_full assert driver.need_download() feed = self.config.iface_cache.get_feed(foo_iface_uri) feed.feeds = [model.Feed('/BadFeed', None, False)] logger.setLevel(logging.ERROR) assert driver.need_download() # Triggers warning logger.setLevel(logging.WARN)
def testDryRun(self): with output_suppressed(): run_server('Hello', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B', 'HelloWorld.tgz') self.config.handler.dry_run = True driver = Driver(requirements = Requirements('http://localhost:8000/Hello'), config = self.config) assert driver.need_download() sys.stdin = Reply("Y\n") sys.stdout = StringIO() download_and_execute(driver, ['Hello'], main = 'Missing', dry_run = True) out = sys.stdout.getvalue() assert '[dry-run] would trust key DE937DD411906ACF7C263B396FCF121BE2390E0B for localhost:8000' in out, out assert '[dry-run] would cache feed http://localhost:8000/Hello as ' in out, out assert '[dry-run] would store implementation as ' in out, out assert '[dry-run] would execute:' in out, out
def testRejectKeyXML(self): with output_suppressed(): run_server('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') driver = Driver(requirements = Requirements('http://example.com:8000/Hello.xml'), config = self.config) assert driver.need_download() sys.stdin = Reply("N\n") try: download_and_execute(driver, ['Hello']) assert 0 except model.SafeException as ex: if "has no usable implementations" not in str(ex): raise ex if "Not signed with a trusted key" not in str(self.config.handler.ex): raise self.config.handler.ex = None
def testRejectKeyXML(self): with output_suppressed(): run_server('Hello.xml', '6FCF121BE2390E0B.gpg', '/key-info/key/DE937DD411906ACF7C263B396FCF121BE2390E0B') driver = Driver(requirements = Requirements('http://example.com:8000/Hello.xml'), config = self.config) assert driver.need_download() sys.stdin = Reply("N\n") try: download_and_execute(driver, ['Hello']) assert 0 except model.SafeException as ex: if "No known implementations at all" not in str(ex): raise ex if "Not signed with a trusted key" not in str(self.config.handler.ex): raise self.config.handler.ex = None
def testNeedDL(self): self.cache_iface(foo_iface_uri, """<?xml version="1.0" ?> <interface last-modified="0" uri="%s" main='ThisBetterNotExist' xmlns="http://zero-install.sourceforge.net/2004/injector/interface"> <name>Foo</name> <summary>Foo</summary> <description>Foo</description> <implementation version='1.0' id='sha1=123'> <archive href='http://foo/foo.tgz' size='100'/> </implementation> </interface>""" % foo_iface_uri) driver = Driver(requirements = Requirements(foo_iface_uri), config = self.config) self.config.network_use = model.network_full recalculate(driver) assert driver.need_download() assert driver.solver.ready
def _check_for_updates(requirements, verbose): from zeroinstall.injector.driver import Driver from zeroinstall.injector.config import load_config background_handler = BackgroundHandler(requirements.interface_uri, requirements.interface_uri) background_config = load_config(background_handler) root_iface = background_config.iface_cache.get_interface(requirements.interface_uri).get_name() background_handler.title = root_iface driver = Driver(config = background_config, requirements = requirements) info(_("Checking for updates to '%s' in a background process"), root_iface) if verbose: background_handler.notify("Zero Install", _("Checking for updates to '%s'...") % root_iface, timeout = 1) network_state = background_handler.get_network_state() if network_state not in (_NetworkState.NM_STATE_CONNECTED_SITE, _NetworkState.NM_STATE_CONNECTED_GLOBAL): info(_("Not yet connected to network (status = %d). Sleeping for a bit..."), network_state) import time time.sleep(120) if network_state in (_NetworkState.NM_STATE_DISCONNECTED, _NetworkState.NM_STATE_ASLEEP): info(_("Still not connected to network. Giving up.")) sys.exit(1) else: info(_("NetworkManager says we're on-line. Good!")) background_config.freshness = 0 # Don't bother trying to refresh when getting the interface refresh = driver.solve_with_downloads(force = True) # (causes confusing log messages) tasks.wait_for_blocker(refresh) # We could even download the archives here, but for now just # update the interfaces. if not driver.need_download(): if verbose: background_handler.notify("Zero Install", _("No updates to download."), timeout = 1) sys.exit(0) background_handler.notify("Zero Install", _("Updates ready to download for '%s'.") % root_iface, timeout = 1) _exec_gui(requirements.interface_uri, '--refresh', '--systray') sys.exit(1)
def testUnknownAlg(self): self.cache_iface(foo_iface_uri, """<?xml version="1.0" ?> <interface uri="%s" xmlns="http://zero-install.sourceforge.net/2004/injector/interface"> <name>Foo</name> <summary>Foo</summary> <description>Foo</description> <implementation main='.' id='unknown=123' version='1.0'> <archive href='http://foo/foo.tgz' size='100'/> </implementation> </interface>""" % foo_iface_uri) self.config.fetcher = fetch.Fetcher(self.config) driver = Driver(requirements = Requirements(foo_iface_uri), config = self.config) try: assert driver.need_download() download_and_execute(driver, []) except model.SafeException as ex: assert "Use '_' not '=' for new algorithms, in unknown=123" in str(ex), ex
def get_selections(config, options, iface_uri, select_only, download_only, test_callback): """Get selections for iface_uri, according to the options passed. Will switch to GUI mode if necessary. @param options: options from OptionParser @param iface_uri: canonical URI of the interface @param select_only: return immediately even if the selected versions aren't cached @param download_only: wait for stale feeds, and display GUI button as Download, not Run @return: the selected versions, or None if the user cancels @rtype: L{selections.Selections} | None """ if options.offline: config.network_use = model.network_offline iface_cache = config.iface_cache # Try to load it as a feed. If it is a feed, it'll get cached. If not, it's a # selections document and we return immediately. maybe_selections = iface_cache.get_feed(iface_uri, selections_ok = True) if isinstance(maybe_selections, selections.Selections): if not select_only: blocker = maybe_selections.download_missing(config) if blocker: logging.info(_("Waiting for selected implementations to be downloaded...")) tasks.wait_for_blocker(blocker) return maybe_selections r = requirements.Requirements(iface_uri) r.parse_options(options) driver = Driver(config = config, requirements = r) # Note that need_download() triggers a solve if options.refresh or options.gui: # We could run immediately, but the user asked us not to can_run_immediately = False else: if select_only: # --select-only: we only care that we've made a selection, not that we've cached the implementations driver.need_download() can_run_immediately = driver.solver.ready else: can_run_immediately = not driver.need_download() stale_feeds = [feed for feed in driver.solver.feeds_used if not os.path.isabs(feed) and # Ignore local feeds (note: file might be missing too) not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds iface_cache.is_stale(iface_cache.get_feed(feed), config.freshness)] if download_only and stale_feeds: can_run_immediately = False if can_run_immediately: if stale_feeds: if config.network_use == model.network_offline: logging.debug(_("No doing background update because we are in off-line mode.")) else: # There are feeds we should update, but we can run without them. # Do the update in the background while the program is running. from zeroinstall.injector import background background.spawn_background_update(driver, options.verbose > 0) return driver.solver.selections # If the user didn't say whether to use the GUI, choose for them. if options.gui is None and os.environ.get('DISPLAY', None): options.gui = True # If we need to download anything, we might as well # refresh all the feeds first. options.refresh = True logging.info(_("Switching to GUI mode... (use --console to disable)")) if options.gui: gui_args = driver.requirements.get_as_options() if download_only: # Just changes the button's label gui_args.append('--download-only') if options.refresh: gui_args.append('--refresh') if options.verbose: gui_args.insert(0, '--verbose') if options.verbose > 1: gui_args.insert(0, '--verbose') if options.with_store: for x in options.with_store: gui_args += ['--with-store', x] if select_only: gui_args.append('--select-only') from zeroinstall import helpers sels = helpers.get_selections_gui(iface_uri, gui_args, test_callback) if not sels: return None # Aborted else: # Note: --download-only also makes us stop and download stale feeds first. downloaded = driver.solve_and_download_impls(refresh = options.refresh or download_only or False, select_only = select_only) if downloaded: tasks.wait_for_blocker(downloaded) sels = driver.solver.selections return sels
def _check_for_updates(self, sels): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. We also schedule a background update from time-to-time anyway. @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network utime = self._get_mtime('last-checked', warn_if_missing=True) last_solve = max(self._get_mtime('last-solve', warn_if_missing=False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) feed = iface_cache.get_feed(sel.feed) if not feed: raise IOError("Input %s missing; update" % sel.feed) else: if feed.local_path: yield feed.local_path else: yield (feed.url, feed.last_modified) # Per-feed configuration yield basedir.load_first_config( namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve iface_cache = self.config.iface_cache try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item mtime = os.stat(path).st_mtime if mtime and mtime > last_solve: logger.info( "Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config=self.config, requirements=self.get_requirements()) if driver.need_download(): # Continue with the current (hopefully cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked=False) self._touch('last-solve') # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing=False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time( ): logger.info( "Tried to check within last hour; not trying again now") need_update = False if need_update: self.set_last_check_attempt() from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def testLocalArchive(self): local_iface = os.path.join(mydir, 'LocalArchive.xml') with open(local_iface, 'rb') as stream: root = qdom.parse(stream) # Not local => error feed = model.ZeroInstallFeed(root) impl = feed.implementations['impl1'] blocker = self.config.fetcher.download_impls([impl], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert "Relative URL 'HelloWorld.tgz' in non-local feed" in str(ex), ex feed = model.ZeroInstallFeed(root, local_path = local_iface) # Missing file impl2 = feed.implementations['impl2'] blocker = self.config.fetcher.download_impls([impl2], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert 'tests/IDONTEXIST.tgz' in str(ex), ex # Wrong size impl3 = feed.implementations['impl3'] blocker = self.config.fetcher.download_impls([impl3], self.config.stores) try: tasks.wait_for_blocker(blocker) assert 0 except model.SafeException as ex: assert 'feed says 177, but actually 176 bytes' in str(ex), ex self.config.network_use = model.network_offline r = Requirements(local_iface) r.command = None driver = Driver(requirements = r, config = self.config) driver.need_download() assert driver.solver.ready, driver.solver.get_failure_reason() # Local => OK impl = feed.implementations['impl1'] path = self.config.stores.lookup_maybe(impl.digests) assert not path blocker = self.config.fetcher.download_impls([impl], self.config.stores) tasks.wait_for_blocker(blocker) path = self.config.stores.lookup_any(impl.digests) assert os.path.exists(os.path.join(path, 'HelloWorld')) # Local <file> => OK impl = feed.implementations['impl4'] path = self.config.stores.lookup_maybe(impl.digests) assert not path blocker = self.config.fetcher.download_impls([impl], self.config.stores) tasks.wait_for_blocker(blocker) path = self.config.stores.lookup_any(impl.digests) assert os.path.exists(os.path.join(path, 'archive.tgz'))
def testSelections(self): requirements = Requirements('http://foo/Source.xml') requirements.source = True requirements.command = 'compile' driver = Driver(requirements=requirements, config=self.config) source = self.config.iface_cache.get_interface('http://foo/Source.xml') compiler = self.config.iface_cache.get_interface( 'http://foo/Compiler.xml') self.import_feed(source.uri, 'Source.xml') self.import_feed(compiler.uri, 'Compiler.xml') self.config.network_use = model.network_full #import logging #logging.getLogger().setLevel(logging.DEBUG) assert driver.need_download() def assertSel(s): self.assertEqual('http://foo/Source.xml', s.interface) self.assertEqual(2, len(s.selections)) sels = [(sel.interface, sel) for sel in s.selections.values()] sels.sort() sels = [sel for uri, sel in sels] self.assertEqual('http://foo/Compiler.xml', sels[0].interface) self.assertEqual('http://foo/Source.xml', sels[1].interface) self.assertEqual("sha1=345", sels[0].id) self.assertEqual("1.0", sels[0].version) self.assertEqual('sha1=234', sels[1].id) self.assertEqual("1.0", sels[1].version) self.assertEqual("bar", sels[1].attrs['http://namespace foo']) self.assertEqual("1.0", sels[1].attrs['version']) assert 'version-modifier' not in sels[1].attrs self.assertEqual(0, len(sels[0].bindings)) self.assertEqual(0, len(sels[0].dependencies)) self.assertEqual(3, len(sels[1].bindings)) self.assertEqual('.', sels[1].bindings[0].insert) self.assertEqual('/', sels[1].bindings[1].mount_point) self.assertEqual('source', sels[1].bindings[2].qdom.attrs['foo']) self.assertEqual(1, len(sels[1].dependencies)) dep = sels[1].dependencies[0] self.assertEqual('http://foo/Compiler.xml', dep.interface) self.assertEqual(4, len(dep.bindings)) self.assertEqual('bin', dep.bindings[0].insert) self.assertEqual('PATH', dep.bindings[0].name) self.assertEqual('prepend', dep.bindings[0].mode) assert dep.bindings[0].separator in ';:' self.assertEqual('bin', dep.bindings[1].value) self.assertEqual('NO_PATH', dep.bindings[1].name) self.assertEqual(',', dep.bindings[1].separator) self.assertEqual('bin', dep.bindings[2].insert) self.assertEqual('BINDIR', dep.bindings[2].name) self.assertEqual('replace', dep.bindings[2].mode) foo_binding = dep.bindings[3] self.assertEqual('compiler', foo_binding.qdom.attrs['foo']) self.assertEqual('child', foo_binding.qdom.childNodes[0].name) self.assertEqual('run', foo_binding.command) self.assertEqual(["sha1=345", 'sha256new_345'], sorted(sels[0].digests)) assert driver.solver.ready, driver.solver.get_failure_reason() s1 = driver.solver.selections s1.selections['http://foo/Source.xml'].attrs[ 'http://namespace foo'] = 'bar' assertSel(s1) xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) self.assertEqual(namespaces.XMLNS_IFACE, root.uri) s2 = selections.Selections(root) assertSel(s2)
def get_selections_for(requirements, config, options, select_only, download_only, test_callback): """Get selections for given requirements. @since: 1.9""" if options.offline: config.network_use = model.network_offline iface_cache = config.iface_cache driver = Driver(config=config, requirements=requirements) # Note that need_download() triggers a solve if options.refresh or options.gui: # We could run immediately, but the user asked us not to can_run_immediately = False else: if select_only: # --select-only: we only care that we've made a selection, not that we've cached the implementations driver.need_download() can_run_immediately = driver.solver.ready else: can_run_immediately = not driver.need_download() stale_feeds = [ feed for feed in driver.solver.feeds_used if not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds iface_cache.is_stale(feed, config.freshness) ] if download_only and stale_feeds: can_run_immediately = False if can_run_immediately: if stale_feeds: if config.network_use == model.network_offline: logger.debug( _("No doing background update because we are in off-line mode." )) else: # There are feeds we should update, but we can run without them. # Do the update in the background while the program is running. from zeroinstall.injector import background background.spawn_background_update(driver, options.verbose) return driver.solver.selections # If we need to download anything, we might as well # refresh all the feeds first. options.refresh = True if options.gui != False: # If the user didn't say whether to use the GUI, choose for them. gui_args = driver.requirements.get_as_options() if download_only: # Just changes the button's label gui_args.append('--download-only') if options.refresh: gui_args.append('--refresh') if options.verbose: gui_args.insert(0, '--verbose') if options.verbose > 1: gui_args.insert(0, '--verbose') if options.with_store: for x in options.with_store: gui_args += ['--with-store', x] if select_only: gui_args.append('--select-only') from zeroinstall import helpers sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, test_callback, use_gui=options.gui) if not sels: return None # Aborted elif sels is helpers.DontUseGUI: sels = None else: sels = None if sels is None: # Note: --download-only also makes us stop and download stale feeds first. downloaded = driver.solve_and_download_impls(refresh=options.refresh or download_only or False, select_only=select_only) if downloaded: tasks.wait_for_blocker(downloaded) sels = driver.solver.selections return sels
def get_selections(config, options, iface_uri, select_only, download_only, test_callback): """Get selections for iface_uri, according to the options passed. Will switch to GUI mode if necessary. @param options: options from OptionParser @param iface_uri: canonical URI of the interface @param select_only: return immediately even if the selected versions aren't cached @param download_only: wait for stale feeds, and display GUI button as Download, not Run @return: the selected versions, or None if the user cancels @rtype: L{selections.Selections} | None """ if options.offline: config.network_use = model.network_offline iface_cache = config.iface_cache # Try to load it as a feed. If it is a feed, it'll get cached. If not, it's a # selections document and we return immediately. maybe_selections = iface_cache.get_feed(iface_uri, selections_ok = True) if isinstance(maybe_selections, selections.Selections): if not select_only: blocker = maybe_selections.download_missing(config) if blocker: logging.info(_("Waiting for selected implementations to be downloaded...")) tasks.wait_for_blocker(blocker) return maybe_selections r = requirements.Requirements(iface_uri) r.parse_options(options) driver = Driver(config = config, requirements = r) # Note that need_download() triggers a solve if options.refresh or options.gui: # We could run immediately, but the user asked us not to can_run_immediately = False else: if select_only: # --select-only: we only care that we've made a selection, not that we've cached the implementations driver.need_download() can_run_immediately = driver.solver.ready else: can_run_immediately = not driver.need_download() stale_feeds = [feed for feed in driver.solver.feeds_used if not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds iface_cache.is_stale(feed, config.freshness)] if download_only and stale_feeds: can_run_immediately = False if can_run_immediately: if stale_feeds: if config.network_use == model.network_offline: logging.debug(_("No doing background update because we are in off-line mode.")) else: # There are feeds we should update, but we can run without them. # Do the update in the background while the program is running. from zeroinstall.injector import background background.spawn_background_update(driver, options.verbose > 0) return driver.solver.selections # If the user didn't say whether to use the GUI, choose for them. if options.gui is None and os.environ.get('DISPLAY', None): options.gui = True # If we need to download anything, we might as well # refresh all the feeds first. options.refresh = True logging.info(_("Switching to GUI mode... (use --console to disable)")) if options.gui: gui_args = driver.requirements.get_as_options() if download_only: # Just changes the button's label gui_args.append('--download-only') if options.refresh: gui_args.append('--refresh') if options.verbose: gui_args.insert(0, '--verbose') if options.verbose > 1: gui_args.insert(0, '--verbose') if options.with_store: for x in options.with_store: gui_args += ['--with-store', x] if select_only: gui_args.append('--select-only') from zeroinstall import helpers sels = helpers.get_selections_gui(iface_uri, gui_args, test_callback) if not sels: return None # Aborted else: # Note: --download-only also makes us stop and download stale feeds first. downloaded = driver.solve_and_download_impls(refresh = options.refresh or download_only or False, select_only = select_only) if downloaded: tasks.wait_for_blocker(downloaded) sels = driver.solver.selections return sels
def get_selections_for(requirements, config, options, select_only, download_only, test_callback): """Get selections for given requirements. @since: 1.9""" if options.offline: config.network_use = model.network_offline iface_cache = config.iface_cache driver = Driver(config = config, requirements = requirements) # Note that need_download() triggers a solve if options.refresh or options.gui: # We could run immediately, but the user asked us not to can_run_immediately = False else: if select_only: # --select-only: we only care that we've made a selection, not that we've cached the implementations driver.need_download() can_run_immediately = driver.solver.ready else: can_run_immediately = not driver.need_download() stale_feeds = [feed for feed in driver.solver.feeds_used if not feed.startswith('distribution:') and # Ignore (memory-only) PackageKit feeds iface_cache.is_stale(feed, config.freshness)] if download_only and stale_feeds: can_run_immediately = False if can_run_immediately: if stale_feeds: if config.network_use == model.network_offline: logger.debug(_("No doing background update because we are in off-line mode.")) else: # There are feeds we should update, but we can run without them. # Do the update in the background while the program is running. from zeroinstall.injector import background background.spawn_background_update(driver, options.verbose) return driver.solver.selections # If we need to download anything, we might as well # refresh all the feeds first. options.refresh = True if options.gui != False: # If the user didn't say whether to use the GUI, choose for them. gui_args = driver.requirements.get_as_options() if download_only: # Just changes the button's label gui_args.append('--download-only') if options.refresh: gui_args.append('--refresh') if options.verbose: gui_args.insert(0, '--verbose') if options.verbose > 1: gui_args.insert(0, '--verbose') if options.with_store: for x in options.with_store: gui_args += ['--with-store', x] if select_only: gui_args.append('--select-only') from zeroinstall import helpers sels = helpers.get_selections_gui(requirements.interface_uri, gui_args, test_callback, use_gui = options.gui) if not sels: return None # Aborted elif sels is helpers.DontUseGUI: sels = None else: sels = None if sels is None: # Note: --download-only also makes us stop and download stale feeds first. downloaded = driver.solve_and_download_impls(refresh = options.refresh or download_only or False, select_only = select_only) if downloaded: tasks.wait_for_blocker(downloaded) sels = driver.solver.selections return sels
def _check_for_updates(self, sels): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. We also schedule a background update from time-to-time anyway. @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network utime = self._get_mtime('last-checked', warn_if_missing = True) last_solve = max(self._get_mtime('last-solve', warn_if_missing = False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) feed = iface_cache.get_feed(sel.feed) if not feed: raise IOError("Input %s missing; update" % sel.feed) else: if feed.local_path: yield feed.local_path else: yield (feed.url, feed.last_modified) # Per-feed configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve iface_cache = self.config.iface_cache try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item mtime = os.stat(path).st_mtime if mtime and mtime > last_solve: logger.info("Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config = self.config, requirements = self.get_requirements()) if driver.need_download(): # Continue with the current (hopefully cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked = False) self._touch('last-solve') # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing = False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time(): logger.info("Tried to check within last hour; not trying again now") need_update = False if need_update: self.set_last_check_attempt() from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def _check_for_updates(self, sels, use_gui): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. If we can't use the current selections, we update in the foreground. We also schedule a background update from time-to-time anyway. @type sels: L{zeroinstall.injector.selections.Selections} @type use_gui: bool @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network if sels: utime = self._get_mtime('last-checked', warn_if_missing = True) last_solve = max(self._get_mtime('last-solve', warn_if_missing = False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) if sel.feed.startswith('distribution:'): # If the package has changed version, we'll detect that below # with get_unavailable_selections. pass elif os.path.isabs(sel.feed): # Local feed yield sel.feed else: # Cached feed cached = basedir.load_first_cache(namespaces.config_site, 'interfaces', model.escape(sel.feed)) if cached: yield cached else: raise IOError("Input %s missing; update" % sel.feed) # Per-feed configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item try: mtime = os.stat(path).st_mtime except OSError as ex: logger.info("Triggering update to {app} due to error: {ex}".format( app = self, path = path, ex = ex)) need_solve = True break if mtime and mtime > last_solve: logger.info("Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True # If any of the saved selections aren't available then we need # to download right now, not later in the background. unavailable_selections = sels.get_unavailable_selections(config = self.config, include_packages = True) if unavailable_selections: logger.info("Saved selections are unusable (missing %s)", ', '.join(str(s) for s in unavailable_selections)) need_solve = True else: # No current selections need_solve = True unavailable_selections = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config = self.config, requirements = self.get_requirements()) if driver.need_download(): if unavailable_selections: return self._foreground_update(driver, use_gui) else: # Continue with the current (cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if old_sels is None or not xmltools.nodes_equal(sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked = False) try: self._touch('last-solve') except OSError as ex: logger.warning("Error checking for updates: %s", ex) # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing = False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time(): logger.info("Tried to check within last hour; not trying again now") need_update = False if need_update: try: self.set_last_check_attempt() except OSError as ex: logger.warning("Error checking for updates: %s", ex) else: from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def _check_for_updates(self, sels, use_gui): """Check whether the selections need to be updated. If any input feeds have changed, we re-run the solver. If the new selections require a download, we schedule one in the background and return the old selections. Otherwise, we return the new selections. If we can select better versions without downloading, we update the app's selections and return the new selections. If we can't use the current selections, we update in the foreground. We also schedule a background update from time-to-time anyway. @type sels: L{zeroinstall.injector.selections.Selections} @type use_gui: bool @return: the selections to use @rtype: L{selections.Selections}""" need_solve = False # Rerun solver (cached feeds have changed) need_update = False # Update over the network if sels: utime = self._get_mtime('last-checked', warn_if_missing=True) last_solve = max( self._get_mtime('last-solve', warn_if_missing=False), utime) # Ideally, this would return all the files which were inputs into the solver's # decision. Currently, we approximate with: # - the previously selected feed files (local or cached) # - configuration files for the selected interfaces # - the global configuration # We currently ignore feeds and interfaces which were # considered but not selected. # Can yield None (ignored), paths or (path, mtime) tuples. # If this throws an exception, we will log it and resolve anyway. def get_inputs(): for sel in sels.selections.values(): logger.info("Checking %s", sel.feed) if sel.feed.startswith('distribution:'): # If the package has changed version, we'll detect that below # with get_unavailable_selections. pass elif os.path.isabs(sel.feed): # Local feed yield sel.feed else: # Cached feed cached = basedir.load_first_cache( namespaces.config_site, 'interfaces', model.escape(sel.feed)) if cached: yield cached else: raise IOError("Input %s missing; update" % sel.feed) # Per-feed configuration yield basedir.load_first_config( namespaces.config_site, namespaces.config_prog, 'interfaces', model._pretty_escape(sel.interface)) # Global configuration yield basedir.load_first_config(namespaces.config_site, namespaces.config_prog, 'global') # If any of the feeds we used have been updated since the last check, do a quick re-solve try: for item in get_inputs(): if not item: continue if isinstance(item, tuple): path, mtime = item else: path = item try: mtime = os.stat(path).st_mtime except OSError as ex: logger.info( "Triggering update to {app} due to error: {ex}" .format(app=self, path=path, ex=ex)) need_solve = True break if mtime and mtime > last_solve: logger.info( "Triggering update to %s because %s has changed", self, path) need_solve = True break except Exception as ex: logger.info("Error checking modification times: %s", ex) need_solve = True need_update = True # Is it time for a background update anyway? if not need_update: staleness = time.time() - utime logger.info("Staleness of app %s is %d hours", self, staleness / (60 * 60)) freshness_threshold = self.config.freshness if freshness_threshold > 0 and staleness >= freshness_threshold: need_update = True # If any of the saved selections aren't available then we need # to download right now, not later in the background. unavailable_selections = sels.get_unavailable_selections( config=self.config, include_packages=True) if unavailable_selections: logger.info("Saved selections are unusable (missing %s)", ', '.join(str(s) for s in unavailable_selections)) need_solve = True else: # No current selections need_solve = True unavailable_selections = True if need_solve: from zeroinstall.injector.driver import Driver driver = Driver(config=self.config, requirements=self.get_requirements()) if driver.need_download(): if unavailable_selections: return self._foreground_update(driver, use_gui) else: # Continue with the current (cached) selections while we download need_update = True else: old_sels = sels sels = driver.solver.selections from zeroinstall.support import xmltools if old_sels is None or not xmltools.nodes_equal( sels.toDOM(), old_sels.toDOM()): self.set_selections(sels, set_last_checked=False) try: self._touch('last-solve') except OSError as ex: logger.warning("Error checking for updates: %s", ex) # If we tried to check within the last hour, don't try again. if need_update: last_check_attempt = self._get_mtime('last-check-attempt', warn_if_missing=False) if last_check_attempt and last_check_attempt + 60 * 60 > time.time( ): logger.info( "Tried to check within last hour; not trying again now") need_update = False if need_update: try: self.set_last_check_attempt() except OSError as ex: logger.warning("Error checking for updates: %s", ex) else: from zeroinstall.injector import background r = self.get_requirements() background.spawn_background_update2(r, False, self) return sels
def testSelections(self): requirements = Requirements('http://foo/Source.xml') requirements.source = True requirements.command = 'compile' driver = Driver(requirements = requirements, config = self.config) source = self.config.iface_cache.get_interface('http://foo/Source.xml') compiler = self.config.iface_cache.get_interface('http://foo/Compiler.xml') self.import_feed(source.uri, 'Source.xml') self.import_feed(compiler.uri, 'Compiler.xml') self.config.network_use = model.network_full #import logging #logging.getLogger().setLevel(logging.DEBUG) assert driver.need_download() def assertSel(s): self.assertEqual('http://foo/Source.xml', s.interface) self.assertEqual(2, len(s.selections)) sels = [(sel.interface, sel) for sel in s.selections.values()] sels.sort() sels = [sel for uri,sel in sels] self.assertEqual('http://foo/Compiler.xml', sels[0].interface) self.assertEqual('http://foo/Source.xml', sels[1].interface) self.assertEqual("sha1=345", sels[0].id) self.assertEqual("1.0", sels[0].version) self.assertEqual('sha1=234', sels[1].id) self.assertEqual("1.0", sels[1].version) self.assertEqual("bar", sels[1].attrs['http://namespace foo']) self.assertEqual("1.0", sels[1].attrs['version']) assert 'version-modifier' not in sels[1].attrs self.assertEqual(0, len(sels[0].bindings)) self.assertEqual(0, len(sels[0].dependencies)) self.assertEqual(3, len(sels[1].bindings)) self.assertEqual('.', sels[1].bindings[0].insert) self.assertEqual('/', sels[1].bindings[1].mount_point) self.assertEqual('source', sels[1].bindings[2].qdom.attrs['foo']) self.assertEqual(1, len(sels[1].dependencies)) dep = sels[1].dependencies[0] self.assertEqual('http://foo/Compiler.xml', dep.interface) self.assertEqual(4, len(dep.bindings)) self.assertEqual('bin', dep.bindings[0].insert) self.assertEqual('PATH', dep.bindings[0].name) self.assertEqual('prepend', dep.bindings[0].mode) assert dep.bindings[0].separator in ';:' self.assertEqual('bin', dep.bindings[1].value) self.assertEqual('NO_PATH', dep.bindings[1].name) self.assertEqual(',', dep.bindings[1].separator) self.assertEqual('bin', dep.bindings[2].insert) self.assertEqual('BINDIR', dep.bindings[2].name) self.assertEqual('replace', dep.bindings[2].mode) foo_binding = dep.bindings[3] self.assertEqual('compiler', foo_binding.qdom.attrs['foo']) self.assertEqual('child', foo_binding.qdom.childNodes[0].name) self.assertEqual('run', foo_binding.command) self.assertEqual(["sha1=345", 'sha256new_345'], sorted(sels[0].digests)) assert driver.solver.ready, driver.solver.get_failure_reason() s1 = driver.solver.selections s1.selections['http://foo/Source.xml'].attrs['http://namespace foo'] = 'bar' assertSel(s1) xml = s1.toDOM().toxml("utf-8") root = qdom.parse(BytesIO(xml)) self.assertEqual(namespaces.XMLNS_IFACE, root.uri) s2 = selections.Selections(root) assertSel(s2)