class RemoteTestCase(unittest.TestCase): """ Helper class to implement unit tests against a remote Zeitgeist process """ @staticmethod def _get_pid(matching_string): p1 = Popen(["pgrep", "-x", "zeitgeist-daemo"], stdout=PIPE, stderr=PIPE) out = p1.communicate()[0] pid = out.decode().split('\n')[0] p2 = Popen(["ps", "--no-headers", "-fp", pid], stderr=PIPE, stdout=PIPE) pid_line = p2.communicate()[0].decode() return pid_line @staticmethod def _safe_start_subprocess(cmd, env, timeout=1, error_callback=None): """ starts `cmd` in a subprocess and check after `timeout` if everything goes well""" args = {'env': env} if not '--verbose-subprocess' in sys.argv: args['stderr'] = PIPE args['stdout'] = PIPE process = Popen(cmd, **args) # give the process some time to wake up time.sleep(timeout) error = process.poll() if error: cmd = " ".join(cmd) error = "'%s' exits with error %i." % (cmd, error) if error_callback: error += " *** %s" % error_callback(*process.communicate()) raise RuntimeError(error) return process @staticmethod def _safe_start_daemon(env=None, timeout=1): if env is None: env = os.environ.copy() def error_callback(stdout, stderr): stderr = stderr.decode() if "--replace" in stderr: return "%r | %s" % ( stderr, RemoteTestCase._get_pid("./src/zeitgeist-daemon").replace( "\n", "|")) else: return stderr return RemoteTestCase._safe_start_subprocess( ("./src/zeitgeist-daemon", "--no-datahub", "--log-level=DEBUG"), env, timeout, error_callback) def __init__(self, methodName): super(RemoteTestCase, self).__init__(methodName) self.daemon = None self.client = None def spawn_daemon(self): self.daemon = self._safe_start_daemon(env=self.env) def kill_daemon(self, kill_signal=signal.SIGKILL): os.kill(self.daemon.pid, kill_signal) return self.daemon.wait() def setUp(self, database_path=None): assert self.daemon is None assert self.client is None self.env = os.environ.copy() self.datapath = tempfile.mkdtemp(prefix="zeitgeist.datapath.") self.env.update({ "ZEITGEIST_DATABASE_PATH": database_path or ":memory:", "ZEITGEIST_DATA_PATH": self.datapath, "XDG_CACHE_HOME": os.path.join(self.datapath, "cache"), }) self.spawn_daemon() # hack to clear the state of the interface ZeitgeistDBusInterface._ZeitgeistDBusInterface__shared_state = {} # Replace the bus connection with a private one for each test case, # so that they don't share signals or other state _set_bus(dbus.SessionBus(private=True)) get_bus().set_exit_on_disconnect(False) self.client = ZeitgeistClient() def tearDown(self): assert self.daemon is not None assert self.client is not None get_bus().close() self.kill_daemon() if 'ZEITGEIST_TESTS_KEEP_TMP' in os.environ: print('\n\nAll temporary files have been preserved in %s\n' \ % self.datapath) else: shutil.rmtree(self.datapath) def insertEventsAndWait(self, events): """ Insert a set of events and spin a mainloop until the async reply is back and return the result - which should be a list of ids. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_ids_and_quit(ids): result.extend(ids) mainloop.quit() self.client.insert_events(events, ids_reply_handler=collect_ids_and_quit) mainloop.run() return result def findEventIdsAndWait(self, event_templates, **kwargs): """ Do search based on event_templates and spin a mainloop until the async reply is back and return the result - which should be a list of ids. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_ids_and_quit(ids): result.extend(ids) mainloop.quit() self.client.find_event_ids_for_templates(event_templates, collect_ids_and_quit, **kwargs) mainloop.run() return result def getEventsAndWait(self, event_ids): """ Request a set of full events and spin a mainloop until the async reply is back and return the result - which should be a list of Events. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): for event in events: if event: event[0][0] = int(event.id) result.extend(events) mainloop.quit() self.client.get_events(event_ids, collect_events_and_quit) mainloop.run() return result def findEventsForTemplatesAndWait(self, event_templates, **kwargs): """ Execute ZeitgeistClient.find_events_for_templates in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): result.extend(events) mainloop.quit() self.client.find_events_for_templates(event_templates, collect_events_and_quit, **kwargs) mainloop.run() return result def findEventsForValuesAndWait(self, *args, **kwargs): """ Execute ZeitgeistClient.find_events_for_value in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): result.extend(events) mainloop.quit() self.client.find_events_for_values(collect_events_and_quit, *args, **kwargs) mainloop.run() return result def deleteEventsAndWait(self, event_ids): """ Delete events given by their id and run a loop until the result containing a timetuple describing the interval of changes is returned. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_timestamp_and_quit(timestamps): result.append(timestamps) mainloop.quit() self.client.delete_events(event_ids, collect_timestamp_and_quit) mainloop.run() return result[0] def findRelatedAndWait(self, subject_uris, num_events, result_type): """ Find related subject uris to given uris and return them. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def callback(uri_list): result.extend(uri_list) mainloop.quit() self.client.find_related_uris_for_uris(subject_uris, callback, num_events=num_events, result_type=result_type) mainloop.run() return result @staticmethod def create_mainloop(timeout=5): class MainLoopWithFailure(object): """ Remember to wrap callbacks using the asyncTestMethod decorator. """ def __init__(self): self._mainloop = GLib.MainLoop() self.failed = False def __getattr__(self, name): return getattr(self._mainloop, name) def fail(self, message): self.failed = True self.failure_message = message mainloop.quit() def run(self): assert self.failed is False self._mainloop.run() if self.failed: raise AssertionError(self.failure_message) mainloop = MainLoopWithFailure() if timeout is not None: def cb_timeout(): mainloop.fail("Timed out -- " "operations not completed in reasonable time.") return False # stop timeout from being called again # Add an arbitrary timeout so this test won't block if it fails GLib.timeout_add_seconds(timeout, cb_timeout) return mainloop @staticmethod def get_plain_event(ev): """ Ensure that an Event instance is a Plain Old Python Object (popo), without DBus wrappings, etc. """ if not ev: return NULL_EVENT for subject in ev.subjects: if not subject.current_uri: subject.current_uri = subject.uri if not subject.current_origin: subject.current_origin = subject.origin popo = [] popo.append(list(map(str, ev[0]))) popo.append([list(map(str, subj)) for subj in ev[1]]) # We need the check here so that if D-Bus gives us an empty # byte array we don't serialize the text "dbus.Array(...)". popo.append(str(ev[2]) if ev[2] else '') return popo def assertEventsEqual(self, ev1, ev2): ev1 = self.get_plain_event(Event(ev1)) ev2 = self.get_plain_event(Event(ev2)) if ev1 is not NULL_EVENT and ev2 is not NULL_EVENT: if (ev1[0][0] and not ev2[0][0]) or (ev2[0][0] and not ev1[0][0]): ev1[0][0] = ev2[0][0] = "" # delete IDs self.assertEqual(ev1, ev2)
class RemoteTestCase (unittest.TestCase): """ Helper class to implement unit tests against a remote Zeitgeist process """ @staticmethod def _get_pid(matching_string): p1 = Popen(["ps", "aux"], stdout=PIPE, stderr=PIPE) p2 = Popen(["grep", matching_string], stdin=p1.stdout, stderr=PIPE, stdout=PIPE) return p2.communicate()[0] @staticmethod def _safe_start_subprocess(cmd, env, timeout=1, error_callback=None): """ starts `cmd` in a subprocess and check after `timeout` if everything goes well""" args = { 'env': env } if not '--verbose-subprocess' in sys.argv: args['stderr'] = PIPE args['stdout'] = PIPE process = Popen(cmd, **args) # give the process some time to wake up time.sleep(timeout) error = process.poll() if error: cmd = " ".join(cmd) error = "'%s' exits with error %i." %(cmd, error) if error_callback: error += " *** %s" %error_callback(*process.communicate()) raise RuntimeError(error) return process @staticmethod def _safe_start_daemon(env=None, timeout=1): if env is None: env = os.environ.copy() def error_callback(stdout, stderr): if "--replace" in stderr: return "%r | %s" %(stderr, RemoteTestCase._get_pid( "./src/zeitgeist-daemon").replace("\n", "|")) else: return stderr return RemoteTestCase._safe_start_subprocess( ("./src/zeitgeist-daemon", "--no-datahub", "--log-level=DEBUG"), env, timeout, error_callback) def __init__(self, methodName): super(RemoteTestCase, self).__init__(methodName) self.daemon = None self.client = None def spawn_daemon(self): self.daemon = self._safe_start_daemon(env=self.env) def kill_daemon(self, kill_signal=signal.SIGKILL): os.kill(self.daemon.pid, kill_signal) return self.daemon.wait() def setUp(self, database_path=None): assert self.daemon is None assert self.client is None self.env = os.environ.copy() self.datapath = tempfile.mkdtemp(prefix="zeitgeist.datapath.") self.env.update({ "ZEITGEIST_DATABASE_PATH": database_path or ":memory:", "ZEITGEIST_DATA_PATH": self.datapath, "XDG_CACHE_HOME": os.path.join(self.datapath, "cache"), }) self.spawn_daemon() # hack to clear the state of the interface ZeitgeistDBusInterface._ZeitgeistDBusInterface__shared_state = {} # Replace the bus connection with a private one for each test case, # so that they don't share signals or other state _set_bus(dbus.SessionBus(private=True)) get_bus().set_exit_on_disconnect(False) self.client = ZeitgeistClient() def tearDown(self): assert self.daemon is not None assert self.client is not None get_bus().close() self.kill_daemon() if 'ZEITGEIST_TESTS_KEEP_TMP' in os.environ: print '\n\nAll temporary files have been preserved in %s\n' \ % self.datapath else: shutil.rmtree(self.datapath) def insertEventsAndWait(self, events): """ Insert a set of events and spin a mainloop until the async reply is back and return the result - which should be a list of ids. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_ids_and_quit(ids): result.extend(ids) mainloop.quit() self.client.insert_events(events, ids_reply_handler=collect_ids_and_quit) mainloop.run() return result def findEventIdsAndWait(self, event_templates, **kwargs): """ Do search based on event_templates and spin a mainloop until the async reply is back and return the result - which should be a list of ids. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_ids_and_quit(ids): result.extend(ids) mainloop.quit() self.client.find_event_ids_for_templates(event_templates, collect_ids_and_quit, **kwargs) mainloop.run() return result def getEventsAndWait(self, event_ids): """ Request a set of full events and spin a mainloop until the async reply is back and return the result - which should be a list of Events. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): for event in events: if event: event[0][0] = int(event.id) result.extend(events) mainloop.quit() self.client.get_events(event_ids, collect_events_and_quit) mainloop.run() return result def findEventsForTemplatesAndWait(self, event_templates, **kwargs): """ Execute ZeitgeistClient.find_events_for_templates in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): result.extend(events) mainloop.quit() self.client.find_events_for_templates( event_templates, collect_events_and_quit, **kwargs) mainloop.run() return result def findEventsForValuesAndWait(self, *args, **kwargs): """ Execute ZeitgeistClient.find_events_for_value in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_events_and_quit(events): result.extend(events) mainloop.quit() self.client.find_events_for_values( collect_events_and_quit, *args, **kwargs) mainloop.run() return result def deleteEventsAndWait(self, event_ids): """ Delete events given by their id and run a loop until the result containing a timetuple describing the interval of changes is returned. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def collect_timestamp_and_quit(timestamps): result.append(timestamps) mainloop.quit() self.client.delete_events(event_ids, collect_timestamp_and_quit) mainloop.run() return result[0] def findRelatedAndWait(self, subject_uris, num_events, result_type): """ Find related subject uris to given uris and return them. This method is basically just a hack to invoke an async method in a blocking manner. """ mainloop = self.create_mainloop() result = [] def callback(uri_list): result.extend(uri_list) mainloop.quit() self.client.find_related_uris_for_uris(subject_uris, callback, num_events=num_events, result_type=result_type) mainloop.run() return result @staticmethod def create_mainloop(timeout=5): class MainLoopWithFailure(object): """ Remember to wrap callbacks using the asyncTestMethod decorator. """ def __init__(self): self._mainloop = gobject.MainLoop() self.failed = False def __getattr__(self, name): return getattr(self._mainloop, name) def fail(self, message): self.failed = True self.failure_message = message mainloop.quit() def run(self): assert self.failed is False self._mainloop.run() if self.failed: raise AssertionError, self.failure_message mainloop = MainLoopWithFailure() if timeout is not None: def cb_timeout(): mainloop.fail("Timed out -- " "operations not completed in reasonable time.") return False # stop timeout from being called again # Add an arbitrary timeout so this test won't block if it fails gobject.timeout_add_seconds(timeout, cb_timeout) return mainloop @staticmethod def get_plain_event(ev): """ Ensure that an Event instance is a Plain Old Python Object (popo), without DBus wrappings, etc. """ if not ev: return NULL_EVENT for subject in ev.subjects: if not subject.current_uri: subject.current_uri = subject.uri if not subject.current_origin: subject.current_origin = subject.origin popo = [] popo.append(map(unicode, ev[0])) popo.append([map(unicode, subj) for subj in ev[1]]) # We need the check here so that if D-Bus gives us an empty # byte array we don't serialize the text "dbus.Array(...)". popo.append(str(ev[2]) if ev[2] else u'') return popo def assertEventsEqual(self, ev1, ev2): ev1 = self.get_plain_event(Event(ev1)) ev2 = self.get_plain_event(Event(ev2)) if ev1 is not NULL_EVENT and ev2 is not NULL_EVENT: if (ev1[0][0] and not ev2[0][0]) or (ev2[0][0] and not ev1[0][0]): ev1[0][0] = ev2[0][0] = "" # delete IDs self.assertEqual(ev1, ev2)
class SoftwareCenterZeitgeist(): """ simple wrapper around zeitgeist """ def __init__(self): try: self.zg_client = ZeitgeistClient() except Exception as e: logging.warn("can not get zeitgeist client: '%s'" % e) self.zg_client = None def get_usage_counter(self, application, callback, timerange=None): """Request the usage count as integer for the given application. When the request is there, "callback" is called. A optional timerange like [time.time(), time.time() - 30*24*60*60] can also be specified """ # helper def _callback(event_ids): callback(len(event_ids)) # no client or empty query -> empty result if not self.zg_client or not application: callback(0) return # the app we are looking for application = "application://"+application.split("/")[-1] # the event_templates e1 = Event.new_for_values( actor=application, interpretation=Interpretation.MODIFY_EVENT.uri) e2 = Event.new_for_values( actor=application, interpretation=Interpretation.CREATE_EVENT.uri) # run it self.zg_client.find_event_ids_for_templates( [e1, e2], _callback, timerange=timerange, num_events=0) def get_popular_mimetypes(self, callback, num=3): """ get the "num" (default to 3) most popular mimetypes based on the last 1000 events that zeitgeist recorded and call "callback" with [(count1, "mime1"), (count2, "mime2"), ...] as arguement """ def _callback(events): # gather mimetypes = {} for event in events: if event.subjects is None: continue mimetype = event.subjects[0].mimetype if not mimetype in mimetypes: mimetypes[mimetype] = 0 mimetypes[mimetype] += 1 # return early if empty results = [] if not mimetypes: callback([]) # convert to result and sort for k, v in mimetypes.items(): results.append([v, k]) results.sort(reverse = True) # tell the client about it callback(results[:num]) # no zeitgeist if not self.zg_client: return # trigger event (actual processing is done in _callback) # FIXME: investigate how result_type MostRecentEvents or # MostRecentSubjects would affect the results self.zg_client.find_events_for_templates( [], _callback, num_events=1000, result_type=ResultType.MostRecentEvents)
class SoftwareCenterZeitgeist(): """ simple wrapper around zeitgeist """ def __init__(self): try: self.zg_client = ZeitgeistClient() except Exception as e: logging.warn("can not get zeitgeist client: '%s'" % e) self.zg_client = None def get_usage_counter(self, application, callback, timerange=None): """Request the usage count as integer for the given application. When the request is there, "callback" is called. A optional timerange like [time.time(), time.time() - 30*24*60*60] can also be specified """ # helper def _callback(event_ids): callback(len(event_ids)) # no client or empty query -> empty result if not self.zg_client or not application: callback(0) return # the app we are looking for application = "application://" + application.split("/")[-1] # the event_templates e1 = Event.new_for_values( actor=application, interpretation=Interpretation.MODIFY_EVENT.uri) e2 = Event.new_for_values( actor=application, interpretation=Interpretation.CREATE_EVENT.uri) # run it self.zg_client.find_event_ids_for_templates([e1, e2], _callback, timerange=timerange, num_events=0) def get_popular_mimetypes(self, callback, num=3): """ get the "num" (default to 3) most popular mimetypes based on the last 1000 events that zeitgeist recorded and call "callback" with [(count1, "mime1"), (count2, "mime2"), ...] as arguement """ def _callback(events): # gather mimetypes = {} for event in events: if event.subjects is None: continue mimetype = event.subjects[0].mimetype if not mimetype in mimetypes: mimetypes[mimetype] = 0 mimetypes[mimetype] += 1 # return early if empty results = [] if not mimetypes: callback([]) # convert to result and sort for k, v in mimetypes.items(): results.append([v, k]) results.sort(reverse=True) # tell the client about it callback(results[:num]) # no zeitgeist if not self.zg_client: return # trigger event (actual processing is done in _callback) # FIXME: investigate how result_type MostRecentEvents or # MostRecentSubjects would affect the results self.zg_client.find_events_for_templates( [], _callback, num_events=1000, result_type=ResultType.MostRecentEvents)