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)