From d931dca5799ee1e88ce327bf28424f9739f4ad87 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Fri, 9 Jun 2006 17:23:42 -0400 Subject: [PATCH] Main bits of presence service refactor --- sugar/presence/Buddy.py | 127 ++++++++++ sugar/presence/PresenceService.py | 373 ++++++++++++++++++++++++++++++ sugar/presence/Service.py | 208 +++++++++++++++++ 3 files changed, 708 insertions(+) create mode 100644 sugar/presence/Buddy.py create mode 100644 sugar/presence/PresenceService.py create mode 100644 sugar/presence/Service.py diff --git a/sugar/presence/Buddy.py b/sugar/presence/Buddy.py new file mode 100644 index 00000000..5201a047 --- /dev/null +++ b/sugar/presence/Buddy.py @@ -0,0 +1,127 @@ +import pwd +import os + +import pygtk +pygtk.require('2.0') +import gtk + + +#from sugar import env + +PRESENCE_SERVICE_TYPE = "_presence_olpc._tcp" + + +class Buddy(object): + """Represents another person on the network and keeps track of the + activities and resources they make available for sharing.""" + def __init__(self, service): + self._services = {} + self._nick_name = service.get_name() + self._address = service.get_address() + self._valid = False + self._icon = None + self.add_service(service) + + def add_service(self, service): + """Adds a new service to this buddy's service list.""" + if service.get_type() in self._services.keys(): + return + self._services.keys[service.get_type()] = service + # FIXME: send out signal for new service found + if service.get_type() == PRESENCE_SERVICE_TYPE: + # A buddy isn't valid until its official presence + # service has been found and resolved + self._valid = True + + def remove_service(self, service): + """Remove a service from a buddy; ie, the activity was closed + or the buddy went away.""" + if service.get_type() in self._services.keys(): + del self._services[service.get_type()] + if service.get_type() == PRESENCE_SERVICE_TYPE: + self._valid = False + + def is_valid(self): + """Return whether the buddy is valid or not. A buddy is + not valid until its official presence service has been found + and successfully resolved.""" + return self._valid + + def get_icon_pixbuf(self): + if self._icon: + pbl = gtk.gdk.PixbufLoader() + pbl.write(self._icon) + pbl.close() + return pbl.get_pixbuf() + else: + return None + + def get_icon(self): + """Return the buddies icon, if any.""" + return self._icon + + def get_address(self): + return self._address + + def add_service(self, service): + if service.get_name() != self._nick_name: + return False + if service.get_address() != self._address: + return False + if self._services.has_key(service.get_type()): + return False + self._services[service.get_type()] = service + + def remove_service(self, stype): + if self._services.has_key(stype): + del self._services[stype] + + def get_service(self, stype): + if self._services.has_key(stype): + return self._services[stype] + return None + + def get_nick_name(self): + return self._nick_name + + def set_icon(self, icon): + """Can only set icon for other buddies. The Owner + takes care of setting it's own icon.""" + self._icon = icon + # FIXME: do callbacks for icon-changed + + +class Owner(Buddy): + """Class representing the owner of this machine/instance.""" + def __init__(self): + nick = env.get_nick_name() + if not nick: + nick = pwd.getpwuid(os.getuid())[0] + if not nick or not len(nick): + nick = "n00b" + + Buddy.__init__(self) + + user_dir = env.get_user_dir() + if not os.path.exists(user_dir): + try: + os.makedirs(user_dir) + except OSError: + print 'Could not create user directory.' + + for fname in os.listdir(user_dir): + if not fname.startswith("buddy-icon."): + continue + fd = open(os.path.join(user_dir, fname), "r") + self._icon = fd.read() + fd.close() + break + + def set_icon(self, icon): + """Can only set icon in constructor for now.""" + pass + + def add_service(self, service): + """Do nothing here, since all services we need to know about + are registered with us by our group.""" + pass diff --git a/sugar/presence/PresenceService.py b/sugar/presence/PresenceService.py new file mode 100644 index 00000000..4dbbf0e3 --- /dev/null +++ b/sugar/presence/PresenceService.py @@ -0,0 +1,373 @@ +import threading +import avahi, dbus, dbus.glib, dbus.dbus_bindings, gobject +import Buddy +import Service +import os + + +ACTION_SERVICE_APPEARED = 'appeared' +ACTION_SERVICE_DISAPPEARED = 'disappeared' + +class PresenceService(object): + """Object providing information about the presence of Buddies + and what activities they make available to others.""" + + __lock = threading.Lock() + __instance = None + + def get_instance(): + """Return, creating if needed, the singleton PresenceService + object.""" + PresenceService.__lock.acquire() + if not PresenceService.__instance: + PresenceService.__instance = PresenceService() + PresenceService.__lock.release() + return PresenceService.__instance + get_instance = staticmethod(get_instance) + + def __init__(self, debug=False): + self._debug = debug + self._lock = threading.Lock() + self._started = False + + # nick -> Buddy: buddies we've found + self._buddies = {} + # group UID -> Group: groups we've found + self._groups = {} + + # All the mdns service types we care about + self._allowed_service_types = [] + + # Keep track of stuff we're already browsing with ZC + self._service_type_browsers = {} + self._service_browsers = {} + + # We only resolve services that our clients are interested in; + # but we store unresolved services so that when a client does + # become interested in a new service type, we can quickly + # resolve it + self._unresolved_services = [] + + self._bus = dbus.SystemBus() + self._server = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, + avahi.DBUS_PATH_SERVER), avahi.DBUS_INTERFACE_SERVER) + + def start(self): + """Start the presence service by kicking off service discovery.""" + self._lock.acquire() + if self._started: + self._lock.release() + return + self._started = True + self._lock.release() + + # Always browse .local + self._new_domain_cb(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "local") + + # Connect to Avahi and start looking for stuff + domain_browser = self._server.DomainBrowserNew(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, "", avahi.DOMAIN_BROWSER_BROWSE, dbus.UInt32(0)) + db = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, domain_browser), avahi.DBUS_INTERFACE_DOMAIN_BROWSER) + db.connect_to_signal('ItemNew', self._new_domain_cb_glue) + + def set_debug(self, debug): + self._debug = debug + + def _log(self, msg): + """Simple logger.""" + if self._debug: + print "PresenceService(%d): %s" % (os.getpid(), msg) + + def _resolve_service_error_handler(self, err): + self._log("error resolving service: %s" % err) + + def _find_service(self, slist, name=None, stype=None, domain=None, address=None, port=None): + """Search a list of services for ones matching certain criteria.""" + found = [] + for service in slist: + if name and service.get_name() != name: + continue + if stype and service.get_type() != stype: + continue + if domain and service.get_domain() != domain: + continue + if address and service.get_address() != address: + continue + if port and service.get_port() != port: + continue + found.append(service) + return found + + def _resolve_service_reply_cb(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + """When the service discovery finally gets here, we've got enough information about the + service to assign it to a buddy.""" + self._log("resolved service '%s' type '%s' domain '%s' to %s:%s" % (name, stype, domain, address, port)) + + # If this service was previously unresolved, remove it from the + # unresolved list + found = self._find_service(self._unresolved_services, name=name, + stype=stype, domain=domain) + if not len(found): + return False + + for service in found: + self._unresolved_services.remove(service) + + # Update the service now that it's been resolved + service = found[0] + service.set_address(address) + service.set_port(port) + service.set_properties(txt) + + # Once a service is resolved, we match it up to an existing buddy, + # or create a new Buddy if this is the first service known about the buddy + added = was_valid = False + try: + buddy = self._buddies[name] + was_valid = buddy.is_valid() + added = buddy.add_service(service) + except KeyError: + buddy = Buddy.Buddy(service) + self._buddies[name] = buddy + added = True + if not was_valid and buddy.is_valid(): + # FIXME: send out "new buddy" signals + pass + if added: + # FIXME: send out buddy service added signals + pass + return False + + def _resolve_service_reply_cb_glue(self, interface, protocol, name, stype, domain, host, aprotocol, address, port, txt, flags): + gobject.idle_add(self._resolve_service_reply_cb, interface, protocol, + name, stype, domain, host, aprotocol, address, port, txt, flags) + + def _resolve_service(self, interface, protocol, name, stype, domain, flags): + """Resolve and lookup a ZeroConf service to obtain its address and TXT records.""" + # Ask avahi to resolve this particular service + self._server.ResolveService(int(interface), int(protocol), name, + stype, domain, avahi.PROTO_UNSPEC, dbus.UInt32(0), # use flags here maybe? + reply_handler=self._resolve_service_reply_cb_glue, + error_handler=self._resolve_service_error_handler) + return False + + def _service_appeared_cb(self, interface, protocol, name, stype, domain, flags): + self._log("found service '%s' (%d) of type '%s' in domain '%s' on %i.%i." % (name, flags, stype, domain, interface, protocol)) + + # Add the service to our unresolved services list + found = self._find_service(self._unresolved_services, name=name, + stype=stype, domain=domain) + if not len(found): + service = Service.Service(name, stype, domain) + self._unresolved_services.append(service) + + # If we care about the service right now, resolve it + if stype in self._allowed_service_types or stype == Buddy.PRESENCE_SERVICE_TYPE: + gobject.idle_add(self._resolve_service, interface, protocol, name, stype, domain, flags) + return False + + def _service_appeared_cb_glue(self, interface, protocol, name, stype, domain, flags): + gobject.idle_add(self._service_appeared_cb, interface, protocol, name, stype, domain, flags) + + def _service_disappeared_cb(self, interface, protocol, name, stype, domain, flags): + self._log("service '%s' of type '%s' in domain '%s' on %i.%i disappeared." % (name, stype, domain, interface, protocol)) + # Remove the service from our unresolved services list + found = self._find_service(self._unresolved_services, name=name, + stype=stype, domain=domain) + + buddy = None + try: + buddy = self._buddies[name] + except KeyError: + pass + + # Remove the service from the buddy + if buddy: + buddy.remove_service(found[0]) + # FIXME: send buddy service remove signals + if not buddy.is_valid(): + del self._buddies[name] + # FIXME: send buddy disappeared message + + for service in found: + self._unresolved_services.remove(service) + return False + + def _service_disappeared_cb_glue(self, interface, protocol, name, stype, domain, flags): + gobject.idle_add(self._service_disappeared_cb, interface, protocol, name, stype, domain, flags) + + def _new_service_type_cb(self, interface, protocol, stype, domain, flags): + # Are we already browsing this domain for this type? + if self._service_browsers.has_key((interface, protocol, stype, domain)): + return + + # Start browsing for all services of this type in this domain + s_browser = self._server.ServiceBrowserNew(interface, protocol, stype, domain, dbus.UInt32(0)) + browser_obj = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, s_browser), avahi.DBUS_INTERFACE_SERVICE_BROWSER) + self._log("now browsing for services of type '%s' in domain '%s' on %i.%i ..." % (stype, domain, interface, protocol)) + browser_obj.connect_to_signal('ItemNew', self._service_appeared_cb_glue) + browser_obj.connect_to_signal('ItemRemove', self._service_disappeared_cb_glue) + + self._service_browsers[(interface, protocol, stype, domain)] = browser_obj + return False + + def _new_service_type_cb_glue(self, interface, protocol, stype, domain, flags): + gobject.idle_add(self._new_service_type_cb, interface, protocol, stype, domain, flags) + + def _new_domain_cb(self, interface, protocol, domain, flags=0): + """Callback from Avahi when a new domain has been found. Start + browsing the new domain.""" + # Only use .local for now... + if domain != "local": + return + + # Are we already browsing this domain? + if self._service_type_browsers.has_key((interface, protocol, domain)): + return + + # Start browsing this domain for the services its members offer + try: + st_browser = self._server.ServiceTypeBrowserNew(interface, protocol, domain, dbus.UInt32(0)) + browser_obj = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, st_browser), avahi.DBUS_INTERFACE_SERVICE_TYPE_BROWSER) + except dbus.DBusException, exc: + self._log("got exception %s while attempting to browse domain %s on %i.%i" % (domain, interface, protocol)) + str_exc = str(exc) + if str_exc.find("The name org.freedesktop.Avahi was not provided by any .service files") >= 0: + raise Exception("Avahi does not appear to be running. '%s'" % str_exc) + else: + raise exc + self._log("now browsing domain '%s' on %i.%i ..." % (domain, interface, protocol)) + browser_obj.connect_to_signal('ItemNew', self._new_service_type_cb_glue) + self._service_type_browsers[(interface, protocol, domain)] = browser_obj + return False + + def _new_domain_cb_glue(self, interface, protocol, domain, flags=0): + gobject.idle_add(self._new_domain_cb, interface, protocol, domain, flags) + + def track_service_type(self, stype): + """Requests that the Presence service look for and recognize + a certain mDNS service types.""" + if not type(stype) == type(""): + raise ValueError("service type must be a string.") + if stype == Buddy.PRESENCE_SERVICE_TYPE: + return + if stype in self._allowed_service_types: + return + + self._allowed_service_types.append(stype) + + # Find unresolved services that match the service type + # we're now interested in, and resolve them + found = self._find_service(self._unresolved_services, stype=stype) + for service in found: + gobject.idle_add(self._resolve_service, interface, protocol, name, stype, domain, flags) + + def untrack_service_type(self, stype): + """Stop tracking a certain mDNS service.""" + if not type(stype) == type(""): + raise ValueError("service type must be a string.") + if name in self._allowed_service_types: + self._allowed_service_types.remove(stype) + + def register_service(self, service): + """Register a new service, advertising it to other Buddies on the network.""" + rs_name = service.get_name() + rs_stype = service.get_type() + rs_port = service.get_port() + if type(rs_port) != type(1) and rs_port <= 1024: + raise ValueError("invalid service port.") + rs_props = service.get_properties() + self._log("registered service name '%s' type '%s' on port %d with args %s" % (rs_name, rs_stype, rs_port, rs_props)) + + try: + group = dbus.Interface(self._bus.get_object(avahi.DBUS_NAME, self._server.EntryGroupNew()), avahi.DBUS_INTERFACE_ENTRY_GROUP) + info = ["%s=%s" % (k, v) for k, v in rs_props.items()] + group.AddService(avahi.IF_UNSPEC, avahi.PROTO_UNSPEC, 0, rs_name, rs_stype, + "", "", # domain, host (let the system figure it out) + dbus.UInt16(rs_port), info,) + group.Commit() + except dbus.dbus_bindings.DBusException, exc: + # FIXME: ignore local name collisions, since that means + # the zeroconf service is already registered. Ideally we + # should un-register it an re-register with the correct info + if str(exc) == "Local name collision": + pass + self.track_service_type(rs_stype) + return group + + def get_buddy_by_nick_name(self, nick_name): + if self._buddies.has_key(nick_name): + return self._buddies[nick_name] + return None + + def get_buddies(self): + return self._buddies.values() + +################################################################# +# Tests +################################################################# + +import unittest + +ps = None + +class PresenceServiceTestCase(unittest.TestCase): + _DEF_NAME = "Paul" + _DEF_STYPE = Buddy.PRESENCE_SERVICE_TYPE + _DEF_DOMAIN = "local" + _DEF_PORT = 3333 + _DEF_PROPERTIES = {"foo": "bar", "bork": "baz"} + + def testNoServices(self): + """Ensure that no services are found initially.""" + """This test may illegitimately fail if there's another person + on the network running sugar... So its usefulness is somewhat + dubious.""" + import gtk + global ps + buddies = ps.get_buddies() + assert len(buddies) == 0, "A buddy was found without setting tracked services!" + gtk.main_quit() + + def testServiceRegistration(self): + service = Service.Service(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, + address=None, port=self._DEF_PORT, properties=self._DEF_PROPERTIES) + global ps + ps.register_service(service) + # Give the Presence Service some time to find the new service + gobject.timeout_add(2000, self.quitMain) + import gtk + gtk.main() + + def quitMain(self): + import gtk + gtk.main_quit() + + def testServiceDetection(self): + global ps + buddy = ps.get_buddy_by_nick_name("Paul") + assert buddy, "The registered buddy was not found after 2 seconds!" + assert buddy.is_valid(), "The buddy was invalid, since no presence was advertised." + + def addToSuite(suite): + suite.addTest(PresenceServiceTestCase("testNoServices")) + suite.addTest(PresenceServiceTestCase("testServiceRegistration")) + suite.addTest(PresenceServiceTestCase("testServiceDetection")) + addToSuite = staticmethod(addToSuite) + +def runTests(): + suite = unittest.TestSuite() + PresenceServiceTestCase.addToSuite(suite) + runner = unittest.TextTestRunner() + runner.run(suite) + +def main(): + import pygtk, gtk + global ps + ps = PresenceService.get_instance() + ps.set_debug(True) + ps.start() + gobject.timeout_add(4000, runTests) + gtk.main() + +if __name__ == "__main__": + main() diff --git a/sugar/presence/Service.py b/sugar/presence/Service.py new file mode 100644 index 00000000..c709dd85 --- /dev/null +++ b/sugar/presence/Service.py @@ -0,0 +1,208 @@ +import avahi + +def _txt_to_dict(txt): + """Convert an avahi-returned TXT record formatted + as nested arrays of integers (from dbus) into a dict + of key/value string pairs.""" + prop_dict = {} + props = avahi.txt_array_to_string_array(txt) + for item in props: + key = value = None + if '=' not in item: + # No = means a boolean value of true + key = item + value = True + else: + (key, value) = item.split('=') + prop_dict[key] = value + return prop_dict + +class Service(object): + """Encapsulates information about a specific ZeroConf/mDNS + service as advertised on the network.""" + def __init__(self, name, stype, domain, address=None, port=-1, properties=None): + # Validate immutable options + if not name or (type(name) != type("") and type(name) != type(u"")) or not len(name): + raise ValueError("must specify a valid service name.") + + if not stype or (type(stype) != type("") and type(stype) != type(u"")) or not len(stype): + raise ValueError("must specify a service type.") + if not stype.endswith("._tcp") and not stype.endswith("._udp"): + raise ValueError("must specify a TCP or UDP service type.") + + if not domain or (type(domain) != type("") and type(domain) != type(u"")): + raise ValueError("must specify a domain.") + if domain != "local" and domain != u"local": + raise ValueError("must use the 'local' domain (for now).") + + self._name = name + self._stype = stype + self._domain = domain + self._address = None + self.set_address(address) + self._port = -1 + self.set_port(port) + self._properties = {} + self.set_properties(properties) + + def get_name(self): + return self._name + + def get_one_property(self, key): + if key in self._properties.keys(): + return self._properties[key] + return None + + def get_properties(self): + return self._properties + + def set_properties(self, properties): + self._properties = {} + if type(properties) == type([]): + self._properties = _txt_to_dict(properties) + elif type(properties) == type({}): + self._properties = properties + + def get_type(self): + return self._stype + + def get_port(self): + return self._port + + def set_port(self, port): + if type(port) != type(1): + raise ValueError("must specify a valid port number.") + self._port = port + + def get_address(self): + return self._address + + def set_address(self, address): + if address is not None: + if type(address) != type("") and type(address) != type(u""): + raise ValueError("must specify a valid address.") + if not len(address): + raise ValueError("must specify a valid address.") + self._address = address + + def get_domain(self): + return self._domain + + def is_olpc_service(self): + if self._stype.endswith("._olpc._udp") or self._stype.endswith(".olpc._tcp"): + return True + return False + + +################################################################# +# Tests +################################################################# + +import unittest + +class ServiceTestCase(unittest.TestCase): + _DEF_NAME = "foobar" + _DEF_STYPE = "_foo._bar._tcp" + _DEF_DOMAIN = "local" + _DEF_ADDRESS = "1.1.1.1" + _DEF_PORT = 1234 + _DEF_PROPS = {'foobar': 'baz'} + + _STR_TEST_ARGS = [None, 0, [], {}] + + def _test_init_fail(self, name, stype, domain, address, port, properties, fail_msg): + """Test something we expect to fail.""" + try: + service = Service(name, stype, domain, address, port, properties) + except ValueError, exc: + pass + else: + self.fail("expected a ValueError for %s." % fail_msg) + + def testName(self): + for item in self._STR_TEST_ARGS: + self._test_init_fail(item, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS, "invalid name") + + def testType(self): + for item in self._STR_TEST_ARGS: + self._test_init_fail(self._DEF_NAME, item, self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS, "invalid service type") + self._test_init_fail(self._DEF_NAME, "_bork._foobar", self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS, "invalid service type") + + def testDomain(self): + for item in self._STR_TEST_ARGS: + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, item, self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS, "invalid domain") + # Only accept local for now + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, "foobar", self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS, "invalid domain") + + def testAddress(self): + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, [], + self._DEF_PORT, self._DEF_PROPS, "invalid address") + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, {}, + self._DEF_PORT, self._DEF_PROPS, "invalid address") + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, 1234, + self._DEF_PORT, self._DEF_PROPS, "invalid address") + + def testPort(self): + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + [], self._DEF_PROPS, "invalid port") + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + {}, self._DEF_PROPS, "invalid port") + self._test_init_fail(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + "adf", self._DEF_PROPS, "invalid port") + + def testGoodInit(self): + service = Service(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, self._DEF_PROPS) + assert service.get_name() == self._DEF_NAME, "service name wasn't correct after init." + assert service.get_type() == self._DEF_STYPE, "service type wasn't correct after init." + assert service.get_domain() == "local", "service domain wasn't correct after init." + assert service.get_address() == self._DEF_ADDRESS, "service address wasn't correct after init." + assert service.get_port() == self._DEF_PORT, "service port wasn't correct after init." + value = service.get_one_property('foobar') + assert value and value == 'baz', "service property wasn't correct after init." + + def testAvahiProperties(self): + props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101, 61, 50, 54, 48, 49, 53, 52, 51, 57, 53, 50]] + key = "org.freedesktop.Avahi.cookie" + expected_value = "2601543952" + service = Service(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, props) + value = service.get_one_property(key) + assert value and value == expected_value, "service properties weren't correct after init." + value = service.get_one_property('bork') + assert not value, "service properties weren't correct after init." + + def testBoolProperty(self): + props = [[111, 114, 103, 46, 102, 114, 101, 101, 100, 101, 115, 107, 116, 111, 112, 46, 65, 118, 97, 104, 105, 46, 99, 111, 111, 107, 105, 101]] + key = "org.freedesktop.Avahi.cookie" + expected_value = True + service = Service(self._DEF_NAME, self._DEF_STYPE, self._DEF_DOMAIN, self._DEF_ADDRESS, + self._DEF_PORT, props) + value = service.get_one_property(key) + assert value is not None and value == expected_value, "service properties weren't correct after init." + + def addToSuite(suite): + suite.addTest(ServiceTestCase("testName")) + suite.addTest(ServiceTestCase("testType")) + suite.addTest(ServiceTestCase("testDomain")) + suite.addTest(ServiceTestCase("testAddress")) + suite.addTest(ServiceTestCase("testPort")) + suite.addTest(ServiceTestCase("testGoodInit")) + suite.addTest(ServiceTestCase("testAvahiProperties")) + suite.addTest(ServiceTestCase("testBoolProperty")) + addToSuite = staticmethod(addToSuite) + + +def main(): + suite = unittest.TestSuite() + ServiceTestCase.addToSuite(suite) + runner = unittest.TextTestRunner() + runner.run(suite) + +if __name__ == "__main__": + main()