zaheer - in flumotion/branches/platform-3: . flumotion/common flumotion/component flumotion/component/base flumotion/component/consumers/disker flumotion/component/converters/overlay flumotion/component/muxers flumotion/manager flumotion/test flumotion/wizard

flumotion-commit at lists.fluendo.com flumotion-commit at lists.fluendo.com
Tue Jun 12 12:48:56 CEST 2007


Author: zaheer
Date: Tue Jun 12 12:48:51 2007
New Revision: 5152

Modified:
   flumotion/branches/platform-3/ChangeLog
   flumotion/branches/platform-3/TODO
   flumotion/branches/platform-3/configure.ac
   flumotion/branches/platform-3/flumotion/common/config.py
   flumotion/branches/platform-3/flumotion/component/Makefile.am
   flumotion/branches/platform-3/flumotion/component/base/Makefile.am
   flumotion/branches/platform-3/flumotion/component/base/base.xml
   flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.py
   flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.xml
   flumotion/branches/platform-3/flumotion/component/converters/overlay/overlay.py
   flumotion/branches/platform-3/flumotion/component/feedcomponent.py
   flumotion/branches/platform-3/flumotion/component/feedcomponent010.py
   flumotion/branches/platform-3/flumotion/component/muxers/multipart.py
   flumotion/branches/platform-3/flumotion/manager/depgraph.py
   flumotion/branches/platform-3/flumotion/manager/manager.py
   flumotion/branches/platform-3/flumotion/test/test.xml
   flumotion/branches/platform-3/flumotion/test/test_component.py
   flumotion/branches/platform-3/flumotion/test/test_component_httpstreamer.py
   flumotion/branches/platform-3/flumotion/test/test_config.py
   flumotion/branches/platform-3/flumotion/test/test_manager_depgraph.py
   flumotion/branches/platform-3/flumotion/wizard/save.py
Log:
	* TODO:
	* configure.ac:
	* flumotion/common/config.py (buildEatersDict, dictDiff,
	  dictDiffMessageString, ref, FlumotionConfigXML._parseComponent,
	  FlumotionConfigXML._parseFeedId, FlumotionConfigXML._parseEaters,
	  FlumotionConfigXML.parseFeed, FlumotionConfigXML.addFeed,
	  FlumotionConfigXML._parseSources):
	* flumotion/component/Makefile.am:
	* flumotion/component/base/Makefile.am:
	* flumotion/component/base/base.xml:
	* flumotion/component/consumers/disker/disker.py
	  (DiskerMedium.remote_scheduleRecordings, Disker.change_filename,
	  Disker._notify_caps_cb, Disker.configure_pipeline,
	  Disker.eventStarted, Disker.eventStopped):
	* flumotion/component/consumers/disker/disker.xml:
	* flumotion/component/converters/overlay/overlay.py
	  (Overlay.get_pipeline_string):
	* flumotion/component/feedcomponent.py
	  (MultiInputParseLaunchComponent.get_pipeline_string):
	* flumotion/component/feedcomponent010.py (FeedComponent.init,
	  FeedComponent.do_setup, FeedComponent.parseEaterConfig,
	  FeedComponent.get_eater_name_for_feed_id):
	* flumotion/component/muxers/multipart.py:
	* flumotion/manager/depgraph.py (DepGraph.mapEatersToFeeders):
	* flumotion/manager/manager.py (Vishnu.parseFeedId,
	  Vishnu.fixOldEaterConfig, Vishnu.verifyExistingComponentState,
	  Vishnu.makeNewComponentState):
	* flumotion/test/test.xml:
	* flumotion/test/test_component.py (PipelineTest.__init__,
	  PipelineTest.config, TestParser.testOneSource,
	  TestParser.testOneSourceWithout, TestParser.testTwoSources,
	  TestParser.testTwoBoth):
	* flumotion/test/test_component_httpstreamer.py
	  (TestOldProperties.setUp):
	* flumotion/test/test_config.py
	  (TestConfig.testParseComponentsWithEaters,
	  TestConfig.testParseComponentsWithEatersNotSpecified,
	  TestConfig.testParseComponentsWithEatersDeprecatedWay,
	  TestConfig.testParseComponentsWithTwoEaters,
	  TestConfig.testParseComponentsWithTwoEatersDeprecatedWay,
	  TestConfig.testParseComponentsWithMultipleEater,
	  TestConfig.testParseComponentsWithMultipleEaterDeprecatedWay,
	  AdminConfigTest.testUnknownSocket, TestDictDiff,
	  TestDictDiff.assertOND, TestDictDiff.testSimple,
	  TestDictDiff.testRecursive, TestDictDiff.testHumanReadable,
	  TestDictDiff.test):
	* flumotion/test/test_manager_depgraph.py
	  (testDepGraph._createComponent):
	* flumotion/wizard/save.py (Component.toXML):
	Merge platform-3-a3.



Modified: flumotion/branches/platform-3/ChangeLog
==============================================================================
--- flumotion/branches/platform-3/ChangeLog	(original)
+++ flumotion/branches/platform-3/ChangeLog	Tue Jun 12 12:48:51 2007
@@ -1,3 +1,56 @@
+2007-06-12  Zaheer Abbas Merali  <zaheerabbas at merali dot org>
+
+	* TODO:
+	* configure.ac:
+	* flumotion/common/config.py (buildEatersDict, dictDiff,
+	  dictDiffMessageString, ref, FlumotionConfigXML._parseComponent,
+	  FlumotionConfigXML._parseFeedId, FlumotionConfigXML._parseEaters,
+	  FlumotionConfigXML.parseFeed, FlumotionConfigXML.addFeed,
+	  FlumotionConfigXML._parseSources):
+	* flumotion/component/Makefile.am:
+	* flumotion/component/base/Makefile.am:
+	* flumotion/component/base/base.xml:
+	* flumotion/component/consumers/disker/disker.py
+	  (DiskerMedium.remote_scheduleRecordings, Disker.change_filename,
+	  Disker._notify_caps_cb, Disker.configure_pipeline,
+	  Disker.eventStarted, Disker.eventStopped):
+	* flumotion/component/consumers/disker/disker.xml:
+	* flumotion/component/converters/overlay/overlay.py
+	  (Overlay.get_pipeline_string):
+	* flumotion/component/feedcomponent.py
+	  (MultiInputParseLaunchComponent.get_pipeline_string):
+	* flumotion/component/feedcomponent010.py (FeedComponent.init,
+	  FeedComponent.do_setup, FeedComponent.parseEaterConfig,
+	  FeedComponent.get_eater_name_for_feed_id):
+	* flumotion/component/muxers/multipart.py:
+	* flumotion/manager/depgraph.py (DepGraph.mapEatersToFeeders):
+	* flumotion/manager/manager.py (Vishnu.parseFeedId,
+	  Vishnu.fixOldEaterConfig, Vishnu.verifyExistingComponentState,
+	  Vishnu.makeNewComponentState):
+	* flumotion/test/test.xml:
+	* flumotion/test/test_component.py (PipelineTest.__init__,
+	  PipelineTest.config, TestParser.testOneSource,
+	  TestParser.testOneSourceWithout, TestParser.testTwoSources,
+	  TestParser.testTwoBoth):
+	* flumotion/test/test_component_httpstreamer.py
+	  (TestOldProperties.setUp):
+	* flumotion/test/test_config.py
+	  (TestConfig.testParseComponentsWithEaters,
+	  TestConfig.testParseComponentsWithEatersNotSpecified,
+	  TestConfig.testParseComponentsWithEatersDeprecatedWay,
+	  TestConfig.testParseComponentsWithTwoEaters,
+	  TestConfig.testParseComponentsWithTwoEatersDeprecatedWay,
+	  TestConfig.testParseComponentsWithMultipleEater,
+	  TestConfig.testParseComponentsWithMultipleEaterDeprecatedWay,
+	  AdminConfigTest.testUnknownSocket, TestDictDiff,
+	  TestDictDiff.assertOND, TestDictDiff.testSimple,
+	  TestDictDiff.testRecursive, TestDictDiff.testHumanReadable,
+	  TestDictDiff.test):
+	* flumotion/test/test_manager_depgraph.py
+	  (testDepGraph._createComponent):
+	* flumotion/wizard/save.py (Component.toXML):
+	Merge platform-3-a3.
+
 2007-06-11  Andy Wingo  <wingo at pobox.com>
 
 	* flumotion/worker/checks/package.xml: 

Modified: flumotion/branches/platform-3/TODO
==============================================================================
--- flumotion/branches/platform-3/TODO	(original)
+++ flumotion/branches/platform-3/TODO	Tue Jun 12 12:48:51 2007
@@ -16,7 +16,7 @@
 * MEDIUM	add api for admins to "subscribe" to admin methods in the
 		manager, so only relevant commands get sent
 
-* EASY		deprecate <source> tag and require base= attribute to <comp>
+* EASY		require base= attribute to <comp>
 
 * EASY		rename components, files, ... to be uniform
 		(e.g. web-camera -> webcam, ...)

Modified: flumotion/branches/platform-3/configure.ac
==============================================================================
--- flumotion/branches/platform-3/configure.ac	(original)
+++ flumotion/branches/platform-3/configure.ac	Tue Jun 12 12:48:51 2007
@@ -174,6 +174,8 @@
 flumotion/component/effects/colorbalance/Makefile
 flumotion/component/effects/volume/Makefile
 flumotion/component/encoders/Makefile
+flumotion/component/combiners/Makefile
+flumotion/component/combiners/switch/Makefile
 flumotion/component/consumers/Makefile
 flumotion/component/consumers/disker/Makefile
 flumotion/component/consumers/httpstreamer/Makefile

Modified: flumotion/branches/platform-3/flumotion/common/config.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/common/config.py	(original)
+++ flumotion/branches/platform-3/flumotion/common/config.py	Tue Jun 12 12:48:51 2007
@@ -41,6 +41,101 @@
 # all these string values should result in True
 BOOL_TRUE_VALUES = ['True', 'true', '1', 'Yes', 'yes']
 
+def buildEatersDict(eatersList, eaterDefs):
+    """Build a eaters dict suitable for forming part of a component
+    config.
+
+    @param eatersList: List of eaters. For example,
+                       [('default', 'othercomp:feeder')] says that our
+                       eater 'default' will be fed by the feed
+                       identified by the feedId 'othercomp:feeder'.
+    @type  eatersList: List of (eaterName, feedId)
+    @param  eaterDefs: The set of allowed and required eaters
+    @type   eaterDefs: List of
+                       L{flumotion.common.registry.RegistryEntryEater}
+    """
+    eaters = {}
+    for eater, feedId in eatersList:
+        if eater is None:
+            # cope with old <source> entries
+            eater = eaterDefs[0].getName()
+        feeders = eaters.get(eater, [])
+        if feedId in feeders:
+            raise ConfigError("Already have a feedId %s eating "
+                              "from %s", feedId, eater)
+        feeders.append(feedId)
+        eaters[eater] = feeders
+    for e in eaterDefs:
+        eater = e.getName()
+        if e.getRequired() and not eater in eaters:
+            raise ConfigError("Component wants to eat on %s,"
+                              " but no feeders specified."
+                              % (e.getName(),))
+        if not e.getMultiple() and len(eaters.get(eater, [])) > 1:
+            raise ConfigError("Component does not support multiple "
+                              "sources feeding %s (%r)"
+                              % (eater, eaters[eater]))
+    return eaters        
+
+def dictDiff(old, new, onlyOld=None, onlyNew=None, diff=None,
+             keyBase=None):
+    """Compute the difference between two config dicts.
+
+    @returns: 3 tuple: (onlyOld, onlyNew, diff) where:
+              onlyOld is a list of (key, value), representing key-value
+              pairs that are only in old;
+              onlyNew is a list of (key, value), representing key-value
+              pairs that are only in new;
+              diff is a list of (key, oldValue, newValue), representing
+              keys with different values in old and new; and
+              key is a tuple of strings representing the recursive key
+              to get to a value. For example, ('foo', 'bar') represents
+              the value d['foo']['bar'] on a dict d.
+    """
+    # key := tuple of strings
+
+    if onlyOld is None:
+        onlyOld = [] # key, value
+        onlyNew = [] # key, value
+        diff = [] # key, oldvalue, newvalue
+        keyBase = ()
+
+    for k in old:
+        key = (keyBase + (k,))
+        if k not in new:
+            onlyOld.append((key, old[k]))
+        elif old[k] != new[k]:
+            if isinstance(old[k], dict) and isinstance(new[k], dict):
+                dictDiff(old[k], new[k], onlyOld, onlyNew, diff, key)
+            else:
+                diff.append((key, old[k], new[k]))
+
+    for k in new:
+        key = (keyBase + (k,))
+        if k not in old:
+            onlyNew.append((key, new[k]))
+
+    return onlyOld, onlyNew, diff
+
+def dictDiffMessageString((old, new, diff), oldLabel='old',
+                          newLabel='new'):
+    def ref(label, k):
+        return "%s%s: '%s'" % (label,
+                               ''.join(["[%r]" % (subk,)
+                                        for subk in k[:-1]]),
+                               k[-1])
+
+    out = []
+    for k, v in old:
+        out.append('Only in %s = %r' % (ref(oldLabel, k), v))
+    for k, v in new:
+        out.append('Only in %s = %r' % (ref(newLabel, k), v))
+    for k, oldv, newv in diff:
+        out.append('Value mismatch:')
+        out.append('    %s = %r' % (ref(oldLabel, k), oldv))
+        out.append('    %s = %r' % (ref(newLabel, k), newv))
+    return '\n'.join(out)
+
 class ConfigEntryComponent(log.Loggable):
     "I represent a <component> entry in a planet config file"
     nice = 0
@@ -391,7 +486,10 @@
         @rtype: L{ConfigEntryComponent}
         """
         # <component name="..." type="..." worker="...">
-        #   <source>*
+        #   <source>...</source>* DEPRECATED
+        #   <eater name="...">
+        #     <feed>...</feed>
+        #   </eater>*
         #   <property name="name">value</property>*
         # </component>
         
@@ -451,7 +549,7 @@
                 "unknown component type: %s" % type)
         
         possible_node_names = ['source', 'clock-master', 'property',
-                               'plugs']
+                               'plugs', 'eater']
         for subnode in node.childNodes:
             if subnode.nodeType == Node.COMMENT_NODE:
                 continue
@@ -466,10 +564,23 @@
         # let the component know what its feeds should be called
         config['feed'] = defs.getFeeders()
 
-        sources = self._parseSources(node, defs)
-        if sources:
-            config['source'] = sources
-
+        eaters = self._parseEaters(node, defs)
+        if not eaters:
+            sources = self._parseSources(node, defs)
+            if sources:
+                config['source'] = sources
+                # assign sources up to first (and only eater)
+                # if more than one eater, parseSources would have raised
+                # a ConfigError
+                config['eater'] = {defs.getEaters()[0].getName():sources}
+        else:
+            # get sources as a list of strings from the dict of eaters
+            sources = []
+            for e in eaters:
+                sources.extend(eaters[e])
+            if sources:
+                config['source'] = sources
+            config['eater'] = eaters
         config['clock-master'] = self._parseClockMaster(node)
         config['plugs'] = self._parsePlugs(node, defs.getSockets())
 
@@ -627,19 +738,75 @@
             raise ConfigError("<%s> value not specified" % name)
         return value
 
+    def _parseFeedId(self, feedId):
+        if feedId.find(':') == -1:
+            return "%s:default" % feedId
+        else:
+            return feedId
+
+    def _parseEaters(self, node, defs):
+        # <eater name="eater-name">
+        #   <feed>feeding-component:feed-name</feed>*
+        # </eater>
+        eaters = dict([(x.getName(), x) for x in defs.getEaters()])
+
+        nodes = {}
+        hasSourceNodes = False
+        for subnode in node.childNodes:
+            if subnode.nodeName == 'eater':
+                name, = self.parseAttributes(subnode, ('name',))
+                if nodes.has_key(name):
+                    raise ConfigError("Component %s should not have "
+                        "multiple eater nodes configured with same name:"
+                        " %s" % (node.nodeName, name))
+                feedNodes = []
+                def parseFeed(feedNode):
+                    # <feed>feeding-component:feed-name</feed>
+                    svs = self.get_string_values([feedNode])
+                    feeds = []
+                    for sv in svs:
+                        feeds.append(self._parseFeedId(sv))
+                    return feeds
+                def addFeed(feeds):
+                    feedNodes.extend(feeds)
+                self.parseFromTable(subnode, {'feed': (parseFeed,addFeed)})
+                nodes[name] = feedNodes
+            elif subnode.nodeName == 'source':
+                hasSourceNodes = True
+
+        # for backwards compatibility
+        if len(nodes) == 0 and hasSourceNodes:
+            return nodes
+
+        for e in eaters:
+            if eaters[e].getRequired() and not nodes.has_key(e):
+                raise ConfigError("Component %s wants to eat on %s, but no "
+                                  "eater with name: %s specified." % (
+                                  node.nodeName, e, e))
+            if not eaters[e].getMultiple() and nodes.has_key(e):
+                if len(nodes[e]) > 1:
+                    raise ConfigError("Component %s does not support multiple "
+                        "sources feeding %s (%r)"
+                        % (node.nodeName, e, nodes[e]))
+        return nodes
+
     def _parseSources(self, node, defs):
+        # deprecated in favour of eater tag
         # <source>feeding-component:feed-name</source>
         eaters = dict([(x.getName(), x) for x in defs.getEaters()])
 
+        if len(eaters) > 1:
+            raise ConfigError("Component %s has many eater names specified "
+                "in the registry, the <source> tag cannot be used for this "
+                "and is now deprecated. Use the <eater> tag." % (node.nodeName))
         nodes = []
         for subnode in node.childNodes:
             if subnode.nodeName == 'source':
                 nodes.append(subnode)
         strings = self.get_string_values(nodes)
 
-        # at this point we don't support assigning certain sources to
-        # certain eaters -- a problem to fix later. for now take the
-        # union of the properties.
+        # assigning certain sources to certain eaters can be done
+        # with the <eater> tag, the <source> tag is now deprecated
         required = [x for x in eaters.values() if x.getRequired()]
         multiple = [x for x in eaters.values() if x.getMultiple()]
 
@@ -647,12 +814,17 @@
             raise ConfigError("Component %s wants to eat on %s, but no "
                               "source specified"
                               % (node.nodeName, eaters.keys()[0]))
-        elif len(strings) > 1 and not multiple:
+        if len(strings) > 1 and not multiple:
             raise ConfigError("Component %s does not support multiple "
                               "sources feeding %s (%r)"
                               % (node.nodeName, eaters.keys()[0], strings))
-
-        return strings
+        if len(strings) > 0:
+            self.warning("The <source> tag is deprecated. Please use the "
+                "<eater> tag now")
+        ret = []
+        for s in strings:
+            ret.append(self._parseFeedId(s))
+        return ret
             
     def _parseClockMaster(self, node):
         nodes = []

Modified: flumotion/branches/platform-3/flumotion/component/Makefile.am
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/Makefile.am	(original)
+++ flumotion/branches/platform-3/flumotion/component/Makefile.am	Tue Jun 12 12:48:51 2007
@@ -16,6 +16,7 @@
 SUBDIRS = \
 	base \
 	bouncers \
+	combiners \
 	consumers \
 	converters \
 	effects \

Modified: flumotion/branches/platform-3/flumotion/component/base/Makefile.am
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/base/Makefile.am	(original)
+++ flumotion/branches/platform-3/flumotion/component/base/Makefile.am	Tue Jun 12 12:48:51 2007
@@ -1,6 +1,12 @@
 include $(top_srcdir)/common/python.mk
-                                                                                
-component_PYTHON = __init__.py admin_gtk.py admin_text.py http.py
+
+component_PYTHON = __init__.py \
+	admin_gtk.py \
+	admin_text.py \
+	http.py \
+	scheduler.py \
+	watcher.py
+
 componentdir = $(libdir)/flumotion/python/flumotion/component/base
 component_DATA = base.xml feeders.glade eaters.glade
 

Modified: flumotion/branches/platform-3/flumotion/component/base/base.xml
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/base/base.xml	(original)
+++ flumotion/branches/platform-3/flumotion/component/base/base.xml	Tue Jun 12 12:48:51 2007
@@ -35,5 +35,26 @@
         </directory>
       </directories>
     </bundle>
+    <bundle name="base-scheduler">
+      <dependencies>
+        <dependency name="flumotion" />
+        <dependency name="base-watcher" />
+      </dependencies>
+      <directories>
+        <directory name="flumotion/component/base">
+          <filename location="scheduler.py" />
+        </directory>
+      </directories>
+    </bundle>
+    <bundle name="base-watcher">
+      <dependencies>
+        <dependency name="flumotion" />
+      </dependencies>
+      <directories>
+        <directory name="flumotion/component/base">
+          <filename location="watcher.py" />
+        </directory>
+      </directories>
+    </bundle>
   </bundles>
 </registry> 

Modified: flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.py	Tue Jun 12 12:48:51 2007
@@ -42,6 +42,23 @@
 
 __all__ = ['Disker']
 
+
+"""
+Disker has a property 'ical-schedule'. This allows an ical file to be
+specified in the config and have recordings scheduled based on events.
+This file will be monitored for changes and events reloaded if this
+happens.
+
+The filename of a recording started from an ical file will be produced
+via passing the ical event summary through strftime, so that an archive
+can encode the date and time that it was begun.
+
+The time that will be given to strftime will be given in the timezone of
+the ical event. In practice this will either be UTC or the local time of
+the machine running the disker, as the ical scheduler does not
+understand arbitrary timezones.
+"""
+
 try:
     # icalendar and dateutil modules needed for scheduling recordings
     from icalendar import Calendar
@@ -62,7 +79,9 @@
         self.comp.change_filename(filenameTemplate)
 
     def remote_scheduleRecordings(self, ical):
-        self.comp.parse_ical(ical)
+        if HAS_ICAL:
+            cal = Calendar.from_string(ical)
+            self.addEvents(self.comp.icalScheduler.parseCalendar(cal))
 
     # called when admin ui wants updated state (current filename info)
     def remote_notifyState(self):
@@ -158,11 +177,12 @@
             mime += ";boundary=ThisRandomString"
         return mime
     
-    def change_filename(self, filenameTemplate=None):
+    def change_filename(self, filenameTemplate=None, timeOrTuple=None):
         """
         @param filenameTemplate: strftime formatted string to decide filename
+        @param timeOrTuple: a valid time to pass to strftime, defaulting
+        to time.localtime(). A 9-tuple may be passed instead.
         """
-        self.debug("change_filename()")
         mime = self.get_mime()
         if mime == 'application/ogg':
             ext = 'ogg'
@@ -201,9 +221,9 @@
         if not filenameTemplate:
             filenameTemplate = self._defaultFilenameTemplate
         filename = "%s.%s" % (time.strftime(filenameTemplate,
-            time.localtime()), ext)
+            timeOrTuple or time.localtime()), ext)
         self.location = os.path.join(self.directory, filename)
-
+        self.info("Changing filename to %s", self.location)
         try:
             self.file = open(self.location, 'a')
         except IOError, e:
@@ -276,7 +296,8 @@
         self.caps = caps
 
         if new and self._recordAtStart:
-            reactor.callLater(0, self.change_filename)
+            reactor.callLater(0, self.change_filename, 
+                self._startFilenameTemplate)
 
     # callback for when a client is removed so we can figure out
     # errors
@@ -306,90 +327,35 @@
         self._recordAtStart = properties.get('start-recording', True)
         self._defaultFilenameTemplate = properties.get('filename', 
             '%s.%%Y%%m%%d-%%H%%M%%S' % self.getName())
+        self._startFilenameTemplate = self._defaultFilenameTemplate
         icalfn = properties.get('ical-schedule')
-        if icalfn:
-            ical = open(icalfn, "rb").read()
-            self.parse_ical(ical)
-            self._recordAtStart = False
+        if HAS_ICAL and icalfn:
+            from flumotion.component.base import scheduler
+            self.icalScheduler = scheduler.ICalScheduler(open(
+                icalfn, 'r'))
+            self.icalScheduler.subscribe(self.eventStarted,
+                self.eventStopped)
+            currentEvents = self.icalScheduler.getCurrentEvents()
+            if currentEvents:
+                self._startFilenameTemplate = currentEvents[0]
+                self._recordAtStart = True
+            else:
+                self._recordAtStart = False
+        else:
+            self.warning("An ical file has been specified for "
+                         "scheduling but the necessary modules "
+                         "dateutil and/or icalendar are not installed")
 
         sink = self.get_element('fdsink')
         sink.get_pad('sink').connect('notify::caps', self._notify_caps_cb)
         # connect to client-removed so we can detect errors in file writing
         sink.connect('client-removed', self._client_removed_cb)
 
-    # add code that lets recordings be schedules
-    # TODO: resolve overlapping events
-    def schedule_recording(self, whenStart, whenEnd, recur=None, 
-                           filenameTemplate=None):
-        """
-        Sets a recording to start at a time in the future for a specified
-        duration.
-        @param whenStart time of when to start recording
-        @type whenStart datetime
-        @param whenEnd time of when to end recording
-        @type whenEnd datetime
-        @param recur recurrence rule
-        @type recur icalendar.props.vRecur
-        @param filenameTemplate strftime formatted string to decide filename
-        @type filenameTemplate string
-        """
-        now = datetime.now()
-
-        startRecurRule = None
-        endRecurRule = None
-
-        if recur:
-            self.debug("Have a recurrence rule, parsing")
-            # create dateutil.rrule from the recurrence rules
-            startRecurRule = rrule.rrulestr(recur.ical(), dtstart=whenStart)
-            endRecurRule = rrule.rrulestr(recur.ical(), dtstart=whenEnd) 
-            if now >= whenStart:
-                self.debug("Initial start before now (%r), finding new starts",
-                    whenStart)
-                whenStart = startRecurRule.after(now)
-                whenEnd = endRecurRule.after(now)
-                self.debug("New start is now %r", whenStart)
-
-        if now < whenStart:
-            start = whenStart - now
-            startSecs = start.days * 86400 + start.seconds
-            self.debug("scheduling a recording %d seconds away", startSecs)
-            reactor.callLater(startSecs, 
-                self.start_scheduled_recording, startRecurRule, whenStart,
-                filenameTemplate)
-            end = whenEnd - now
-            endSecs = end.days * 86400 + end.seconds
-            reactor.callLater(endSecs, 
-                self.stop_scheduled_recording, endRecurRule, whenEnd)
-        else:
-            self.warning("attempt to schedule in the past!")
-
-    def start_scheduled_recording(self, recurRule, when, filenameTemplate):
-        self.change_filename(filenameTemplate)
-        if recurRule:
-            now = datetime.now()
-            nextTime = recurRule.after(when)
-            recurInterval = nextTime - now
-            self.debug("recurring start interval: %r", recurInterval)
-            recurIntervalSeconds = recurInterval.days * 86400 + \
-                recurInterval.seconds
-            self.debug("recurring start in %d seconds", recurIntervalSeconds)
-            reactor.callLater(recurIntervalSeconds, 
-                self.start_scheduled_recording,
-                recurRule, nextTime, filenameTemplate)
+    def eventStarted(self, event):
+        self.change_filename(event.content, event.start.timetuple())
 
-    def stop_scheduled_recording(self, recurRule, when):
+    def eventStopped(self, event):
         self.stop_recording()
-        if recurRule:
-            now = datetime.now()
-            nextTime = recurRule.after(when)
-            recurInterval = nextTime - now
-            recurIntervalSeconds = recurInterval.days * 86400 + \
-                recurInterval.seconds
-            self.debug("recurring stop in %d seconds", recurIntervalSeconds)
-            reactor.callLater(recurIntervalSeconds, 
-                self.stop_scheduled_recording,
-                recurRule, nextTime)
 
     def parse_ical(self, icsStr):
         if HAS_ICAL:

Modified: flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.xml
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.xml	(original)
+++ flumotion/branches/platform-3/flumotion/component/consumers/disker/disker.xml	Tue Jun 12 12:48:51 2007
@@ -77,6 +77,7 @@
         <dependencies>
             <dependency name="component"/>
             <dependency name="disker-base"/>
+	    <dependency name="base-scheduler"/>
         </dependencies>
 
         <directories>

Modified: flumotion/branches/platform-3/flumotion/component/converters/overlay/overlay.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/converters/overlay/overlay.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/converters/overlay/overlay.py	Tue Jun 12 12:48:51 2007
@@ -44,7 +44,7 @@
         # this got added to ffmpegcolorspace in 0.8.5
         addalpha = 'ffmpegcolorspace'
 
-        source = self.config['source'][0]
+        source = self.config['eater']['default'][0]
         eater = '@ eater:%s @' % source
 
         # the order here is important; to have our eater be the reference

Modified: flumotion/branches/platform-3/flumotion/component/feedcomponent.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/feedcomponent.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/feedcomponent.py	Tue Jun 12 12:48:51 2007
@@ -557,12 +557,13 @@
             self.QUEUE_SIZE_BUFFERS)
 
     def get_pipeline_string(self, properties):
-        sources = self.config['source']
+        eaters = self.config['eater']
 
         pipeline = self.get_muxer_string(properties) + ' '
-        for eater in sources:
-            tmpl = '@ eater:%s @ ! muxer. '
-            pipeline += tmpl % eater
+        for e in eaters:
+            for feed in eaters[e]:
+                tmpl = '@ eater:%s @ ! muxer. '
+                pipeline += tmpl % feed
 
         pipeline += 'muxer.'
 

Modified: flumotion/branches/platform-3/flumotion/component/feedcomponent010.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/feedcomponent010.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/feedcomponent010.py	Tue Jun 12 12:48:51 2007
@@ -368,7 +368,6 @@
         # add extra keys to state
         self.state.addKey('eaterNames') # feedId of eaters
         self.state.addKey('feederNames') # feedId of feeders
-
         # add keys for eaters and feeders uiState
         self._feeders = {} # feeder feedId -> Feeder
         self._eaters = {} # eater feedId -> Eater
@@ -401,6 +400,8 @@
         self._stateChangeDeferreds = {}
 
         self._gotFirstNewSegment = {}
+        # feedId of eater -> eater name as specified in config
+        self._eaterMapping = {}
 
         # multifdsink's get-stats signal had critical bugs before this version
         tcppluginversion = gstreamer.get_plugin_version('tcp')
@@ -425,7 +426,7 @@
         """
         Sets up component.
         """
-        eater_config = self.config.get('source', [])
+        eater_config = self.config.get('eater', {})
         feeder_config = self.config.get('feed', [])
 
         self.debug("feedcomponent.setup(): eater_config %r" % eater_config)
@@ -554,16 +555,17 @@
 
     def parseEaterConfig(self, eater_config):
         # the source feeder names come from the config
-        # they are specified under <component> as <source> elements in XML
+        # they are specified under <eater> as <feed> elements in XML
         # so if they don't specify a feed name, use "default" as the feed name
-        eater_names = []
-        for block in eater_config:
-            eater_name = block
-            if block.find(':') == -1:
-                eater_name = block + ':default'
-            eater_names.append(eater_name)
-        self.debug('parsed eater config, eater feedIds %r' % eater_names)
-        self.eater_names = eater_names
+        # there is also a deprecated way by specifying them under <component>
+        # as <source> elements in XML
+        feed_ids = []
+        for eater in eater_config:
+            for feed in eater_config[eater]:
+                feed_ids.append(feed)
+                self._eaterMapping[feed] = eater
+        self.debug('parsed eater config, eater feedIds %r' % feed_ids)
+        self.eater_names = feed_ids
         self.state.set('eaterNames', self.eater_names)
             
     def parseFeederConfig(self, feeder_config):
@@ -1278,4 +1280,20 @@
                 self._gotFirstNewSegment[feedId] = True
         return True
 
+    def get_eater_name_for_feed_id(self, feedId):
+        """
+        Given a feedId, it will return the eater name that it is in.
+        Unfortunately feedcomponent keys eaters on the feedId so
+        it is impossible to have the same feedId feeding multiple
+        eaters in the same component.
+
+        @param feedId the feedId to get the eater name for
+        @returns the eater name
+        @rtype: string
+        """
+
+        if self._eaterMapping.has_key(feedId):
+            return self._eaterMapping[feedId]
+        return None
+
 pygobject.type_register(FeedComponent)

Modified: flumotion/branches/platform-3/flumotion/component/muxers/multipart.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/component/muxers/multipart.py	(original)
+++ flumotion/branches/platform-3/flumotion/component/muxers/multipart.py	Tue Jun 12 12:48:51 2007
@@ -23,15 +23,6 @@
 
 from flumotion.component import feedcomponent
 
-class Multipart(feedcomponent.ParseLaunchComponent):
-    def get_pipeline_string(self, properties):
-        sources = self.config['source']
-
-        pipeline = 'multipartmux name=muxer '
-        for eater in sources:
-            tmpl = '@ eater:%s @ ! queue ! muxer. '
-            pipeline += tmpl % eater
-
-        pipeline += 'muxer.'
-
-        return pipeline
+class Multipart(feedcomponent.MultiInputParseLaunchComponent):
+    def get_muxer_string(self, properties):
+        return 'multipartmux name=muxer'

Modified: flumotion/branches/platform-3/flumotion/manager/depgraph.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/manager/depgraph.py	(original)
+++ flumotion/branches/platform-3/flumotion/manager/depgraph.py	Tue Jun 12 12:48:51 2007
@@ -200,46 +200,41 @@
             # for this component setup, go through all the feeders in it
             config = eatingComponent.get('config')
 
-            if not config.has_key('source'):
+            if not config.has_key('eater'):
                 # no eaters
                 self.debug("Component %r has no eaters" % eatingComponent)
             else:
-                # source is a list of componentName[:feedName]
+                # eater is a dict of eaterName -> list of componentName[:feedName]
                 # with feedName defaulting to default
-                # FIXME: maybe source should really be eaters and contain
-                # a list of feedId
-                list = config['source']
-
-                # FIXME: there's a bug in config parsing - sometimes this gives
-                # us one string, and sometimes a list of one string, and
-                # sometimes a list
-                if isinstance(list, str):
-                    list = [list, ]
-
-                for source in list:
-                    feederFound = False
-                    feederComponentName = source.split(':')[0]
-                    # find the feeder
-                    for feedingComponent in toSetup:
-                        if feedingComponent.get("name") == feederComponentName:
-                            feederFound = True
-                            try:
-                                self._addEdge(feedingComponent, eatingComponent,
-                                    "COMPONENTSETUP", "COMPONENTSETUP")
-                            except KeyError:
-                                # it is possible for a component to have
-                                # two eaters, each eating from feeders on
-                                # one other component
-                                pass
-                            try:
-                                self._addEdge(feedingComponent, eatingComponent,
-                                    "COMPONENTSTART", "COMPONENTSTART")
-                            except KeyError:
-                                pass
-
-                    if not feederFound:
-                        raise errors.ComponentConfigError(eatingComponent,
-                            "No feeder exists for eater %s" % source)
+                eaters = config['eater']
+
+                for eater in eaters:
+                    for feed in eaters[eater]:
+                        feederFound = False
+                        feederComponentName = feed.split(':')[0]
+                        # find the feeder
+                        for feedingComponent in toSetup:
+                            if feedingComponent.get("name") == feederComponentName:
+                                feederFound = True
+                                try:
+                                    self._addEdge(feedingComponent, eatingComponent,
+                                        "COMPONENTSETUP", "COMPONENTSETUP")
+                                except KeyError:
+                                    # it is possible for a component to have
+                                    # two eaters, each eating from feeders on
+                                    # one other component
+                                    pass
+                                try:
+                                    self._addEdge(feedingComponent, eatingComponent,
+                                        "COMPONENTSTART", "COMPONENTSTART")
+                                except KeyError:
+                                    pass
+
+                        if not feederFound:
+                            raise errors.ComponentConfigError(eatingComponent,
+                                "No feeder exists for eater %s on component %s"
+                                " feeding from %s" % (eater, eatingComponent,
+                                feed))
 
     def whatShouldBeStarted(self):
         """

Modified: flumotion/branches/platform-3/flumotion/manager/manager.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/manager/manager.py	(original)
+++ flumotion/branches/platform-3/flumotion/manager/manager.py	Tue Jun 12 12:48:51 2007
@@ -839,24 +839,62 @@
         #  (2) we don't know anything about this component, but since it
         #      logged in, we will deal with it, at least allowing the
         #      admin to control it.
+        def parseFeedId(feedId): 
+            if feedId.find(':') == -1:
+                return "%s:default" % feedId
+            else:
+                return feedId
+
+        def fixOldEaterConfig(state):
+            # check for components that have no eater dict but a
+            # non-empty source list, and file all these under
+            # eater default
+            eaterConfig = conf.get('eater', {})
+            sourceConfig = conf.get('source', [])
+            if eaterConfig == {} and sourceConfig != []:
+                eaters = registry.getRegistry().getComponent(
+                    conf.get('type')).getEaters()
+                eatersDict = {}
+                try:
+                    eatersTuple = [(None, parseFeedId(s)) for s in sourceConfig]
+                    eatersDict = config.buildEatersDict(eatersTuple, eaters)
+                except errors.ConfigError:
+                    message = messages.Warning(T_(
+                        N_("Component logged in with old deprecated "
+                           "configuration and creating an eaters config "
+                           "caused an error. Restarting this component "
+                           "will result in bad things. Best thing to do "
+                           "is stop the component, restart manager and "
+                           "start it again.")))
+                    state.append('messages', message)
+                conf['eater'] =  eatersDict
+            if sourceConfig:
+                sources = []
+                for s in sourceConfig:
+                    sources.append(parseFeedId(s))
+                conf['source'] = sources
 
         def verifyExistingComponentState(jobState, state):
             # condition (1)
             state.setJobState(jobState)
-
-            if conf and state.get('config') != conf:
-                message = messages.Warning(T_(
-                    N_("Component logged in with stale configuration. "
-                       "Consider stopping this component and restarting "
-                       "the manager.")),
-                    debug=("Expected\n%r\n, but got\n%r;\n"
-                           "updating internal state accordingly." %
-                           (state.get('config'), conf)))
-                self.warning('updating internal component state for %r '
-                             '(changing config from %r to %r)', state,
-                             state.get('config'), conf)
-                state.set('config', conf)
-                state.append('messages', message)
+            if conf:
+                fixOldEaterConfig(state)
+                if state.get('config') != conf:
+                    diff = config.dictDiff(state.get('config'), conf)
+                    diffMsg = config.dictDiffMessageString(diff,
+                                                       'internal conf',
+                                                       'running conf')
+                    message = messages.Warning(T_(
+                        N_("Component logged in with stale configuration. "
+                        "Consider stopping this component and restarting "
+                        "the manager.")),
+                        debug=("Updating internal conf from running conf:\n"
+                           + diffMsg))
+                    self.warning('updating internal component state for %r')
+                    self.debug('changes to conf: %s',
+                               config.dictDiffMessageString(diff))
+                    state.set('config', conf)
+                    state.append('messages', message)
             # if conf is None, then we just created the component and
             # it's not set up yet
 
@@ -867,6 +905,7 @@
 
             if conf:
                 flowName, compName = conf['parent'], conf['name']
+                fixOldEaterConfig(state)
             else:
                 # unfortunately there is a window in which a component does
                 # not have a config. accept that so that an admin can stop
@@ -878,6 +917,7 @@
                         'avatarId': avatar.avatarId,
                         'properties': {}}
 
+
             state.set('name', compName)
             state.set('type', conf['type'])
             state.set('workerRequested', jobState.get('workerName'))

Modified: flumotion/branches/platform-3/flumotion/test/test.xml
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/test.xml	(original)
+++ flumotion/branches/platform-3/flumotion/test/test.xml	Tue Jun 12 12:48:51 2007
@@ -8,14 +8,18 @@
     </component>
   
     <component name="converter-ogg-theora" type="pipeline-converter" worker="worker">
-      <source>producer-video-test</source>
+      <eater name="default">
+        <feed>producer-video-test</feed>
+      </eater>
       <property name="pipeline">
   ffmpegcolorspace ! theoraenc keyframe-force=5 ! oggmux
       </property>
     </component>
   
     <component name="streamer-ogg-theora" type="http-streamer" worker="streamer">
-      <source>converter-ogg-theora</source>
+      <eater name="default">
+        <feed>converter-ogg-theora</feed>
+      </eater>
       <property name="port">8800</property>
       <plugs>
         <plug socket="flumotion.component.plugs.loggers.Logger"

Modified: flumotion/branches/platform-3/flumotion/test/test_component.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/test_component.py	(original)
+++ flumotion/branches/platform-3/flumotion/test/test_component.py	Tue Jun 12 12:48:51 2007
@@ -38,6 +38,10 @@
     def __init__(self, eaters=None, feeders=None, pipeline='test-pipeline'):
         self.__pipeline = pipeline
         self._source = eaters or []
+        if eaters:
+            self._eater = {'default':eaters}
+        else:
+            self._eater = {}
         self._feed = feeders or []
 
         ParseLaunchComponent.__init__(self)
@@ -45,6 +49,7 @@
     def config(self):
         config = {'name': 'fake',
                   'source': self._source,
+                  'eater': self._eater,
                   'feed': self._feed,
                   'plugs': {},
                   'properties': {}}
@@ -210,7 +215,7 @@
             return d
 
     def testOneSource(self):
-        d, pipeline  = pipelineFactory('@eater:foo@ ! bar', ['foo'])
+        d, pipeline  = pipelineFactory('@eater:foo@ ! bar', ['foo:default'])
         if weHaveAnOldTwisted():
             res = unittest.deferredResult(d)
             self.assertEquals(res, '%s ! bar' % self._eater('foo:default'))
@@ -220,7 +225,7 @@
             return d
 
     def testOneSourceWithout(self):
-        d, pipeline = pipelineFactory('bar', ['foo'])
+        d, pipeline = pipelineFactory('bar', ['foo:default'])
         if weHaveAnOldTwisted():
             res = unittest.deferredResult(d)
             self.assertEquals(res, '%s ! bar' % self._eater('foo:default'))
@@ -253,7 +258,7 @@
 
     def testTwoSources(self):
         d, pipeline = pipelineFactory('@eater:foo@ ! @eater:bar@ ! baz', 
-            ['foo', 'bar'])
+            ['foo:default', 'bar:default'])
         if weHaveAnOldTwisted():
             res = unittest.deferredResult(d)
             self.assertEquals(res, '%s ! %s ! baz' % (
@@ -282,7 +287,7 @@
     def testTwoBoth(self):
         d, pipeline = pipelineFactory(
             '@eater:comp1@ ! @eater:comp2@ ! @feeder::feed1@ ! @feeder::feed2@',
-                              ['comp1', 'comp2',],
+                              ['comp1:default', 'comp2:default',],
                               ['feed1', 'feed2'])
         if weHaveAnOldTwisted():
             res = unittest.deferredResult(d)

Modified: flumotion/branches/platform-3/flumotion/test/test_component_httpstreamer.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/test_component_httpstreamer.py	(original)
+++ flumotion/branches/platform-3/flumotion/test/test_component_httpstreamer.py	Tue Jun 12 12:48:51 2007
@@ -44,7 +44,8 @@
             'feed': [],
             'name': 'http-video',
             'parent': 'default',
-            'source': ['muxer-video'],
+            'eater': {'default': ['muxer-video:default']},
+            'source': ['muxer-video:default'],
             'avatarId': '/default/http-video',
             'clock-master': None,
             'plugs': {

Modified: flumotion/branches/platform-3/flumotion/test/test_config.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/test_config.py	(original)
+++ flumotion/branches/platform-3/flumotion/test/test_config.py	Tue Jun 12 12:48:51 2007
@@ -51,6 +51,19 @@
     <component type="test-component-sync-provider">
       <synchronization required="true" clock-priority="130"/>
     </component>
+    <component type="test-component-with-feeder">
+      <feeder name="default" />
+    </component>
+    <component type="test-component-with-one-eater">
+      <eater name="default" required="true" />
+    </component>
+    <component type="test-component-with-two-eaters">
+      <eater name="video" required="true" />
+      <eater name="audio" required="true" />
+    </component>
+    <component type="test-component-with-multiple-eater">
+      <eater name="default" multiple="true" />
+    </component>
   </components>
   <plugs>
     <plug socket="foo.bar" type="frobulator">
@@ -493,6 +506,181 @@
         self.failUnless(entries.has_key('/atmosphere/atmocomp'))
         self.failUnless(entries.has_key('/default/flowcomp'))
 
+    def testParseComponentsWithEaters(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                  <eater name="default">
+                    <feed>prod:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == ["prod:default"])
+        self.failUnless(cons['source'] == ["prod:default"])
+
+    def testParseComponentsWithEatersNotSpecified(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                </component>
+              </flow>
+            </planet>
+            """)
+        self.assertRaises(config.ConfigError, conf.parse)
+
+    def testParseComponentsWithEatersDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-one-eater"
+                           worker="foo">
+                  <source>prod:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == ["prod:default"])
+        self.failUnless(cons['eater']['default'] == ["prod:default"])
+
+    def testParseComponentsWithTwoEaters(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-two-eaters"
+                           worker="foo">
+                  <eater name="video">
+                    <feed>prod:default</feed>
+                  </eater>
+                  <eater name="audio">
+                    <feed>prod2:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('video'))
+        self.failUnless(cons['eater']['video'] == ["prod:default"])
+        self.failUnless(cons['eater'].has_key('audio'))
+        self.failUnless(cons['eater']['audio'] == ['prod2:default'])
+
+    def testParseComponentsWithTwoEatersDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-two-eaters"
+                           worker="foo">
+                  <source>prod:default</source>
+                  <source>prod2:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        self.assertRaises(config.ConfigError, conf.parse)
+
+    def testParseComponentsWithMultipleEater(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-multiple-eater"
+                           worker="foo">
+                  <eater name="default">
+                    <feed>prod:default</feed>
+                    <feed>prod2:default</feed>
+                  </eater>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == [
+            "prod:default", "prod2:default"])
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == [
+            "prod:default", "prod2:default"])
+
+    def testParseComponentsWithMultipleEaterDeprecatedWay(self):
+        conf = ConfigXML(
+            """
+            <planet>
+              <flow name="default">
+                <component name="prod" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="prod2" type="test-component-with-feeder"
+                           worker="foo"/>
+                <component name="cons" type="test-component-with-multiple-eater"
+                           worker="foo">
+                  <source>prod:default</source>
+                  <source>prod2:default</source>
+                </component>
+              </flow>
+            </planet>
+            """)
+        conf.parse()
+        entries = conf.getComponentEntries()
+        self.failUnless(entries.has_key('/default/prod'))
+        self.failUnless(entries.has_key('/default/cons'))
+        cons = entries['/default/cons'].getConfigDict()
+        self.failUnless(cons.has_key('eater'))
+        self.failUnless(cons['eater'].has_key('default'))
+        self.failUnless(cons['eater']['default'] == [
+            "prod:default", "prod2:default"])
+        self.failUnless(cons.has_key('source'))
+        self.failUnless(cons['source'] == [
+            "prod:default", "prod2:default"])
+
     def testGetComponentEntriesWrong(self):
         xml = """
              <planet>
@@ -555,3 +743,89 @@
                '</admin>')
         self.assertRaises(config.ConfigError,
                           lambda: AdminConfig(('foo.bar',), doc))
+
+class TestDictDiff(unittest.TestCase):
+    def assertOND(self, d1, d2, old, new, diff):
+        o, n, d = config.dictDiff(d1, d2)
+        self.assertEquals(old, o)
+        self.assertEquals(new, n)
+        self.assertEquals(diff, d)
+
+    def testSimple(self):
+        ass = self.assertOND
+        ass({}, {}, [], [], [])
+
+        ass({'foo': 'bar'}, {}, [(('foo',), 'bar')], [], [])
+
+        ass({}, {'foo': 'bar'}, [], [(('foo',), 'bar')], [])
+
+        ass({'foo': 'bar'}, {'foo': 'baz'}, [], [], [(('foo',), 'bar', 'baz')])
+
+    def testRecursive(self):
+        ass = self.assertOND
+        ass({}, {}, [], [], [])
+
+        ass({'foo': {'bar': 'baz'}},
+            {},
+            [(('foo',), {'bar':'baz'})],
+            [],
+            [])
+
+        ass({'foo': {'bar': 'baz'}},
+            {'foo': {}},
+            [(('foo','bar'), 'baz')],
+            [],
+            [])
+
+        ass({'foo': {}},
+            {'foo': {'bar': 'baz'}},
+            [],
+            [(('foo','bar'), 'baz')],
+            [])
+
+        ass({},
+            {'foo': {'bar': 'baz'}},
+            [],
+            [(('foo',), {'bar':'baz'})],
+            [])
+
+        ass({'foo': {'bar': 'baz'}},
+            {'foo': {'bar': 'qux'}},
+            [],
+            [],
+            [(('foo','bar'), 'baz', 'qux')])
+
+    def testHumanReadable(self):
+        def test(d1, d2, s):
+            msg = config.dictDiffMessageString(config.dictDiff(d1, d2))
+            self.assertEquals(msg, s)
+
+        test({}, {}, '')
+        test({'foo': 42}, {}, "Only in old: 'foo' = 42")
+        test({}, {'foo': 42}, "Only in new: 'foo' = 42")
+        test({'foo': 17}, {'foo': 42},
+             "Value mismatch:\n"
+             "    old: 'foo' = 17\n"
+             "    new: 'foo' = 42")
+
+        test({'foo': {'bar': 'baz'}},
+             {},
+             "Only in old: 'foo' = {'bar': 'baz'}")
+
+        test({'foo': {'bar': 'baz'}},
+             {'foo': {}},
+             "Only in old['foo']: 'bar' = 'baz'")
+
+        test({'foo': {}},
+             {'foo': {'bar': 'baz'}},
+             "Only in new['foo']: 'bar' = 'baz'")
+
+        test({},
+             {'foo': {'bar': 'baz'}},
+             "Only in new: 'foo' = {'bar': 'baz'}")
+
+        test({'foo': {'bar': 'baz'}},
+             {'foo': {'bar': 'qux'}},
+             "Value mismatch:\n"
+             "    old['foo']: 'bar' = 'baz'\n"
+             "    new['foo']: 'bar' = 'qux'")

Modified: flumotion/branches/platform-3/flumotion/test/test_manager_depgraph.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/test/test_manager_depgraph.py	(original)
+++ flumotion/branches/platform-3/flumotion/test/test_manager_depgraph.py	Tue Jun 12 12:48:51 2007
@@ -40,8 +40,9 @@
         source = []
         for eater in defs[4]:
             source.append(eater)
-        
+       
         conf["source"] = source
+        conf["eater"] = {"default":source}
 
         ret.set("config", conf)
         return ret

Modified: flumotion/branches/platform-3/flumotion/wizard/save.py
==============================================================================
--- flumotion/branches/platform-3/flumotion/wizard/save.py	(original)
+++ flumotion/branches/platform-3/flumotion/wizard/save.py	Tue Jun 12 12:48:51 2007
@@ -78,9 +78,12 @@
         s = '    <component name="%s" type="%s" ' \
             'project="flumotion" version="%s"%s>\n' % (
             self.name, self.type, configure.version, extra)
-
-        for sourceName in self.getFeeders():
-            s += "      <source>%s</source>\n" % sourceName
+        whoIsFeedingUs = self.getFeeders()
+        if len(whoIsFeedingUs) > 0:
+            s += '     <eater name="default">\n'
+            for sourceName in whoIsFeedingUs:
+                s += "      <feed>%s</feed>\n" % sourceName
+            s += '     </eater>\n'
                     
         if self.props:
             s += "\n"


More information about the flumotion-commit mailing list