Browse Source

added error feedback to config_validate() and option to validate partial configurations in cpp version
fixed a bug in Mapelement->str() (it can now print empty map elements)
added tests for python config_data module and fixed a few bugs there


git-svn-id: svn://bind10.isc.org/svn/bind10/trunk@911 e5f2f494-b856-4b98-b285-d166d9295462

Jelte Jansen 15 years ago
parent
commit
3a69c8a7a8

+ 5 - 1
src/bin/auth/auth_srv.cc

@@ -122,7 +122,11 @@ AuthSrv::updateConfig(isc::data::ElementPtr config) {
         // todo: what to do with port change. restart automatically?
         // ignore atm
     //}
-    std::cout << "[XX] auth: new config " << config << std::endl;
+    if (config) {
+        std::cout << "[XX] auth: new config " << config << std::endl;
+    } else {
+        std::cout << "[XX] auth: new config empty" << std::endl;
+    }
     
     return isc::config::createAnswer(0);
 }

+ 5 - 1
src/lib/cc/cpp/data.cc

@@ -487,7 +487,11 @@ MapElement::str()
             ss << ", ";
         }
         ss << "\"" << (*it).first << "\": ";
-        ss << (*it).second->str();
+        if ((*it).second) {
+            ss << (*it).second->str();
+        } else {
+            ss << "None";
+        }
     }
     ss << "}";
     return ss.str();

+ 1 - 1
src/lib/cc/cpp/data.h

@@ -409,7 +409,7 @@ public:
     using Element::setValue;
     bool setValue(std::map<std::string, ElementPtr>& v) { m = v; return true; };
     using Element::get;
-    ElementPtr get(const std::string& s) { return m[s]; };
+    ElementPtr get(const std::string& s) { if (contains(s)) { return m[s]; } else { return ElementPtr();} };
     using Element::set;
     void set(const std::string& s, ElementPtr p) { m[s] = p; };
     using Element::remove;

+ 11 - 2
src/lib/config/cpp/ccsession.cc

@@ -172,12 +172,20 @@ ElementPtr
 ModuleCCSession::handleConfigUpdate(ElementPtr new_config)
 {
     ElementPtr answer;
+    ElementPtr errors = Element::createFromString("[]");
+    std::cout << "handleConfigUpdate " << new_config << std::endl;
     if (!config_handler_) {
         answer = createAnswer(1, module_name_ + " does not have a config handler");
-    } else if (!module_specification_.validate_config(new_config)) {
-        answer = createAnswer(2, "Error in config validation");
+    } else if (!module_specification_.validate_config(new_config, false, errors)) {
+        std::stringstream ss;
+        ss << "Error in config validation: ";
+        BOOST_FOREACH(ElementPtr error, errors->listValue()) {
+            ss << error->stringValue();
+        }
+        answer = createAnswer(2, ss.str());
     } else {
         // handle config update
+        std::cout << "handleConfigUpdate " << new_config << std::endl;
         answer = config_handler_(new_config);
         int rcode;
         parseAnswer(rcode, answer);
@@ -185,6 +193,7 @@ ModuleCCSession::handleConfigUpdate(ElementPtr new_config)
             config_ = new_config;
         }
     }
+    std::cout << "end handleConfigUpdate " << new_config << std::endl;
     return answer;
 }
 

+ 27 - 11
src/lib/config/cpp/module_spec.cc

@@ -209,10 +209,17 @@ ModuleSpec::getModuleName()
 }
 
 bool
-ModuleSpec::validate_config(const ElementPtr data)
+ModuleSpec::validate_config(const ElementPtr data, const bool full)
 {
     ElementPtr spec = module_specification->find("module_spec/config_data");
-    return validate_spec_list(spec, data);
+    return validate_spec_list(spec, data, full, ElementPtr());
+}
+
+bool
+ModuleSpec::validate_config(const ElementPtr data, const bool full, ElementPtr errors)
+{
+    ElementPtr spec = module_specification->find("module_spec/config_data");
+    return validate_spec_list(spec, data, full, errors);
 }
 
 ModuleSpec
@@ -279,28 +286,34 @@ check_type(ElementPtr spec, ElementPtr element)
 }
 
 bool
-ModuleSpec::validate_item(const ElementPtr spec, const ElementPtr data) {
+ModuleSpec::validate_item(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors) {
     if (!check_type(spec, data)) {
         // we should do some proper error feedback here
         // std::cout << "type mismatch; not " << spec->get("item_type") << ": " << data << std::endl;
         // std::cout << spec << std::endl;
+        if (errors) {
+            errors->add(Element::create("Type mismatch"));
+        }
         return false;
     }
     if (data->getType() == Element::list) {
         ElementPtr list_spec = spec->get("list_item_spec");
         BOOST_FOREACH(ElementPtr list_el, data->listValue()) {
             if (!check_type(list_spec, list_el)) {
+                if (errors) {
+                    errors->add(Element::create("Type mismatch"));
+                }
                 return false;
             }
             if (list_spec->get("item_type")->stringValue() == "map") {
-                if (!validate_item(list_spec, list_el)) {
+                if (!validate_item(list_spec, list_el, full, errors)) {
                     return false;
                 }
             }
         }
     }
     if (data->getType() == Element::map) {
-        if (!validate_spec_list(spec->get("map_item_spec"), data)) {
+        if (!validate_spec_list(spec->get("map_item_spec"), data, full, errors)) {
             return false;
         }
     }
@@ -309,18 +322,21 @@ ModuleSpec::validate_item(const ElementPtr spec, const ElementPtr data) {
 
 // spec is a map with item_name etc, data is a map
 bool
-ModuleSpec::validate_spec(const ElementPtr spec, const ElementPtr data) {
+ModuleSpec::validate_spec(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors) {
     std::string item_name = spec->get("item_name")->stringValue();
     bool optional = spec->get("item_optional")->boolValue();
     ElementPtr data_el;
-    
     data_el = data->get(item_name);
+    
     if (data_el) {
-        if (!validate_item(spec, data_el)) {
+        if (!validate_item(spec, data_el, full, errors)) {
             return false;
         }
     } else {
-        if (!optional) {
+        if (!optional && full) {
+            if (errors) {
+                errors->add(Element::create("Non-optional value missing"));
+            }
             return false;
         }
     }
@@ -329,11 +345,11 @@ ModuleSpec::validate_spec(const ElementPtr spec, const ElementPtr data) {
 
 // spec is a list of maps, data is a map
 bool
-ModuleSpec::validate_spec_list(const ElementPtr spec, const ElementPtr data) {
+ModuleSpec::validate_spec_list(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors) {
     ElementPtr cur_data_el;
     std::string cur_item_name;
     BOOST_FOREACH(ElementPtr cur_spec_el, spec->listValue()) {
-        if (!validate_spec(cur_spec_el, data)) {
+        if (!validate_spec(cur_spec_el, data, full, errors)) {
             return false;
         }
     }

+ 7 - 4
src/lib/config/cpp/module_spec.h

@@ -83,12 +83,15 @@ namespace isc { namespace config {
         /// \param data The base \c Element of the data to check
         /// \return true if the data conforms to the specification,
         /// false otherwise.
-        bool validate_config(const ElementPtr data);
+        bool validate_config(const ElementPtr data, const bool full = false);
+
+        /// errors must be of type ListElement
+        bool validate_config(const ElementPtr data, const bool full, ElementPtr errors);
 
     private:
-        bool validate_item(const ElementPtr spec, const ElementPtr data);
-        bool validate_spec(const ElementPtr spec, const ElementPtr data);
-        bool validate_spec_list(const ElementPtr spec, const ElementPtr data);
+        bool validate_item(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors);
+        bool validate_spec(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors);
+        bool validate_spec_list(const ElementPtr spec, const ElementPtr data, const bool full, ElementPtr errors);
 
         ElementPtr module_specification;
     };

+ 5 - 2
src/lib/config/python/isc/config/cfgmgr.py

@@ -230,6 +230,8 @@ class ConfigManager:
             # todo: use api (and check the data against the definition?)
             module_name = cmd[1]
             conf_part = data.find_no_exc(self.config.data, module_name)
+            print("[XX] cfgmgr conf part:")
+            print(conf_part)
             if conf_part:
                 data.merge(conf_part, cmd[2])
                 self.cc.group_sendmsg({ "config_update": conf_part }, module_name)
@@ -246,6 +248,7 @@ class ConfigManager:
                 self.write_config()
         elif len(cmd) == 2:
             # todo: use api (and check the data against the definition?)
+            old_data = self.config.data.copy()
             data.merge(self.config.data, cmd[1])
             # send out changed info
             got_error = False
@@ -262,8 +265,8 @@ class ConfigManager:
                 self.write_config()
                 answer = isc.config.ccsession.create_answer(0)
             else:
-                # TODO rollback changes that did get through?
-                # feed back *all* errors?
+                # TODO rollback changes that did get through, should we re-send update?
+                self.config.data = old_data
                 answer = isc.config.ccsession.create_answer(1, " ".join(err_list))
         else:
             answer = isc.config.ccsession.create_answer(1, "Wrong number of arguments")

+ 60 - 32
src/lib/config/python/isc/config/config_data.py

@@ -26,9 +26,10 @@ import isc.config.module_spec
 class ConfigDataError(Exception): pass
 
 def check_type(spec_part, value):
-    """Returns true if the value is of the correct type given the
-       specification part relevant for the value. spec_part can be
-       retrieved with find_spec()"""
+    """Does nothing if the value is of the correct type given the
+       specification part relevant for the value. Raises an
+       isc.cc.data.DataTypeError exception if not. spec_part can be
+       retrieved with find_spec_part()"""
     if type(spec_part) == list:
         data_type = "list"
     else:
@@ -36,9 +37,9 @@ def check_type(spec_part, value):
 
     if data_type == "integer" and type(value) != int:
         raise isc.cc.data.DataTypeError(str(value) + " is not an integer")
-    elif data_type == "real" and type(value) != double:
+    elif data_type == "real" and type(value) != float:
         raise isc.cc.data.DataTypeError(str(value) + " is not a real")
-    elif data_type == "boolean" and type(value) != boolean:
+    elif data_type == "boolean" and type(value) != bool:
         raise isc.cc.data.DataTypeError(str(value) + " is not a boolean")
     elif data_type == "string" and type(value) != str:
         raise isc.cc.data.DataTypeError(str(value) + " is not a string")
@@ -52,7 +53,7 @@ def check_type(spec_part, value):
         # todo: check types of map contents too
         raise isc.cc.data.DataTypeError(str(value) + " is not a map")
 
-def find_spec(element, identifier):
+def find_spec_part(element, identifier):
     """find the data definition for the given identifier
        returns either a map with 'item_name' etc, or a list of those"""
     if identifier == "":
@@ -65,6 +66,14 @@ def find_spec(element, identifier):
             cur_el = cur_el[id]
         elif type(cur_el) == dict and 'item_name' in cur_el.keys() and cur_el['item_name'] == id:
             pass
+        elif type(cur_el) == dict and 'map_item_spec' in cur_el.keys():
+            found = False
+            for cur_el_item in cur_el['map_item_spec']:
+                if cur_el_item['item_name'] == id:
+                    cur_el = cur_el_item
+                    found = True
+            if not found:
+                raise isc.cc.data.DataNotFoundError(id + " in " + str(cur_el))
         elif type(cur_el) == list:
             found = False
             for cur_el_item in cur_el:
@@ -84,24 +93,30 @@ def spec_name_list(spec, prefix="", recurse=False):
     if prefix != "" and not prefix.endswith("/"):
         prefix += "/"
     if type(spec) == dict:
-        for name in spec:
-            result.append(prefix + name + "/")
-            if recurse:
-                result.extend(spec_name_list(spec[name],name, recurse))
+        if 'map_item_spec' in spec:
+            for map_el in spec['map_item_spec']:
+                name = map_el['item_name']
+                if map_el['item_type'] == 'map':
+                    name += "/"
+                result.append(prefix + name)
+        else:
+            for name in spec:
+                result.append(prefix + name + "/")
+                if recurse:
+                    print("[XX] recurse1")
+                    result.extend(spec_name_list(spec[name],name, recurse))
     elif type(spec) == list:
         for list_el in spec:
             if 'item_name' in list_el:
-                if list_el['item_type'] == dict:
-                    if recurse:
-                        result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
+                if list_el['item_type'] == "map" and recurse:
+                    result.extend(spec_name_list(list_el['map_item_spec'], prefix + list_el['item_name'], recurse))
                 else:
                     name = list_el['item_name']
                     if list_el['item_type'] in ["list", "map"]:
                         name += "/"
-                    result.append(name)
+                    result.append(prefix + name)
     return result
 
-
 class ConfigData:
     """This class stores the module specs and the current non-default
        config values. It provides functions to get the actual value or
@@ -118,11 +133,12 @@ class ConfigData:
     def get_value(self, identifier):
         """Returns a tuple where the first item is the value at the
            given identifier, and the second item is a bool which is
-           true if the value is an unset default"""
+           true if the value is an unset default. Raises an
+           isc.cc.data.DataNotFoundError if the identifier is bad"""
         value = isc.cc.data.find_no_exc(self.data, identifier)
-        if value:
+        if value != None:
             return value, False
-        spec = find_spec(self.specification.get_config_spec(), identifier)
+        spec = find_spec_part(self.specification.get_config_spec(), identifier)
         if spec and 'item_default' in spec:
             return spec['item_default'], True
         return None, False
@@ -144,7 +160,7 @@ class ConfigData:
            all 'sub'options at the given identifier. If recurse is True,
            it will also add all identifiers of all children, if any"""
         if identifier:
-            spec = find_spec(self.specification.get_config_spec(), identifier, recurse)
+            spec = find_spec_part(self.specification.get_config_spec(), identifier)
             return spec_name_list(spec, identifier + "/")
         return spec_name_list(self.specification.get_config_spec(), "", recurse)
 
@@ -183,7 +199,8 @@ class MultiConfigData:
         self._specifications[spec.get_module_name()] = spec
 
     def get_module_spec(self, module):
-        """Returns the ModuleSpec for the module with the given name"""
+        """Returns the ModuleSpec for the module with the given name.
+           If there is no such module, it returns None"""
         if module in self._specifications:
             return self._specifications[module]
         else:
@@ -193,14 +210,16 @@ class MultiConfigData:
         """Returns the specification for the item at the given
            identifier, or None if not found. The first part of the
            identifier (up to the first /) is interpreted as the module
-           name."""
+           name. Returns None if not found."""
         if identifier[0] == '/':
             identifier = identifier[1:]
         module, sep, id = identifier.partition("/")
         try:
-            return find_spec(self._specifications[module].get_config_spec(), id)
+            return find_spec_part(self._specifications[module].get_config_spec(), id)
         except isc.cc.data.DataNotFoundError as dnfe:
             return None
+        except KeyError as ke:
+            return None
 
     # this function should only be called by __request_config
     def _set_current_config(self, config):
@@ -252,7 +271,7 @@ class MultiConfigData:
             identifier = identifier[1:]
         module, sep, id = identifier.partition("/")
         try:
-            spec = find_spec(self._specifications[module].get_config_spec(), id)
+            spec = find_spec_part(self._specifications[module].get_config_spec(), id)
             if 'item_default' in spec:
                 return spec['item_default']
             else:
@@ -268,13 +287,13 @@ class MultiConfigData:
            (local change, current setting, default as specified by the
            specification, or not found at all)."""
         value = self.get_local_value(identifier)
-        if value:
+        if value != None:
             return value, self.LOCAL
         value = self.get_current_value(identifier)
-        if value:
+        if value != None:
             return value, self.CURRENT
         value = self.get_default_value(identifier)
-        if value:
+        if value != None:
             return value, self.DEFAULT
         return None, self.NONE
 
@@ -305,7 +324,7 @@ class MultiConfigData:
             module, sep, id = identifier.partition('/')
             spec = self.get_module_spec(module)
             if spec:
-                spec_part = find_spec(spec.get_config_spec(), id)
+                spec_part = find_spec_part(spec.get_config_spec(), id)
                 print(spec_part)
                 if type(spec_part) == list:
                     for item in spec_part:
@@ -357,12 +376,15 @@ class MultiConfigData:
         return result
 
     def set_value(self, identifier, value):
-        """Set the local value at the given identifier to value"""
+        """Set the local value at the given identifier to value. If
+           there is a specification for the given identifier, the type
+           is checked."""
         spec_part = self.find_spec_part(identifier)
-        check_type(spec_part, value)
+        if spec_part != None:
+            check_type(spec_part, value)
         isc.cc.data.set(self._local_changes, identifier, value)
  
-    def get_config_item_list(self, identifier = None):
+    def get_config_item_list(self, identifier = None, recurse = False):
         """Returns a list of strings containing the item_names of
            the child items at the given identifier. If no identifier is
            specified, returns a list of module names. The first part of
@@ -370,6 +392,12 @@ class MultiConfigData:
            module name"""
         if identifier:
             spec = self.find_spec_part(identifier)
-            return spec_name_list(spec, identifier + "/")
+            return spec_name_list(spec, identifier + "/", recurse)
         else:
-            return self._specifications.keys()
+            if recurse:
+                id_list = []
+                for module in self._specifications:
+                    id_list.extend(spec_name_list(self._specifications[module], module, recurse))
+                return id_list
+            else:
+                return list(self._specifications.keys())

+ 254 - 6
src/lib/config/python/isc/config/config_data_test.py

@@ -28,13 +28,261 @@ class TestConfigData(unittest.TestCase):
             self.data_path = os.environ['CONFIG_TESTDATA_PATH']
         else:
             self.data_path = "../../../testdata"
+        spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.cd = ConfigData(spec)
 
-    def test_module_spec_from_file(self):
-        spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
-        cd = ConfigData(spec)
-        self.assertEqual(cd.specification, spec)
-        self.assertEqual(cd.data, {})
-        self.assertRaises(ConfigDataError, ConfigData, 1)
+    #def test_module_spec_from_file(self):
+    #    spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
+    #    cd = ConfigData(spec)
+    #    self.assertEqual(cd.specification, spec)
+    #    self.assertEqual(cd.data, {})
+    #    self.assertRaises(ConfigDataError, ConfigData, 1)
+
+    def test_check_type(self):
+        config_spec = self.cd.get_module_spec().get_config_spec()
+        spec_part = find_spec_part(config_spec, "item1")
+        check_type(spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, True)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+        
+        spec_part = find_spec_part(config_spec, "item2")
+        check_type(spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, True)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+        
+        spec_part = find_spec_part(config_spec, "item3")
+        check_type(spec_part, True)
+        check_type(spec_part, False)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+        
+        spec_part = find_spec_part(config_spec, "item4")
+        check_type(spec_part, "asdf")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, True)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+        
+        spec_part = find_spec_part(config_spec, "item5")
+        check_type(spec_part, ["a", "b"])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, True)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "a": 1 })
+        
+        spec_part = find_spec_part(config_spec, "item6")
+        check_type(spec_part, { "value1": "aaa", "value2": 2 })
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, 1.1)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, True)
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, "a")
+        self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, [ 1, 2 ])
+        #self.assertRaises(isc.cc.data.DataTypeError, check_type, spec_part, { "value1": 1 })
+
+    def test_find_spec_part(self):
+        config_spec = self.cd.get_module_spec().get_config_spec()
+        spec_part = find_spec_part(config_spec, "item1")
+        self.assertEqual({'item_name': 'item1', 'item_type': 'integer', 'item_optional': False, 'item_default': 1, }, spec_part)
+        self.assertRaises(isc.cc.data.DataNotFoundError, find_spec_part, config_spec, "no_such_item")
+        self.assertRaises(isc.cc.data.DataNotFoundError, find_spec_part, config_spec, "no_such_item/multilevel")
+        spec_part = find_spec_part(config_spec, "item6/value1")
+        #print(spec_part)
+        self.assertEqual({'item_name': 'value1', 'item_type': 'string', 'item_optional': True, 'item_default': 'default'}, spec_part)
+
+    def test_spec_name_list(self):
+        name_list = spec_name_list(self.cd.get_module_spec().get_config_spec())
+        self.assertEqual(['item1', 'item2', 'item3', 'item4', 'item5/', 'item6/'], name_list)
+        name_list = spec_name_list(self.cd.get_module_spec().get_config_spec(), "", True)
+        self.assertEqual(['item1', 'item2', 'item3', 'item4', 'item5/', 'item6/value1', 'item6/value2'], name_list)
+
+    def test_get_value(self):
+        value, default = self.cd.get_value("item1")
+        self.assertEqual(1, value)
+        self.assertEqual(True, default)
+        value, default = self.cd.get_value("item2")
+        self.assertEqual(1.1, value)
+        self.assertEqual(True, default)
+        value, default = self.cd.get_value("item3")
+        self.assertEqual(True, value)
+        self.assertEqual(True, default)
+        value, default = self.cd.get_value("item4")
+        self.assertEqual("test", value)
+        self.assertEqual(True, default)
+        value, default = self.cd.get_value("item5")
+        self.assertEqual(["a", "b"], value)
+        self.assertEqual(True, default)
+        value, default = self.cd.get_value("item6")
+        self.assertEqual({}, value)
+        self.assertEqual(True, default)
+        self.assertRaises(isc.cc.data.DataNotFoundError, self.cd.get_value, "no_such_item")
+        value, default = self.cd.get_value("item6/value2")
+        self.assertEqual(None, value)
+        self.assertEqual(False, default)
+
+    def test_set_local_config(self):
+        self.cd.set_local_config({"item1": 2})
+        value, default = self.cd.get_value("item1")
+        self.assertEqual(2, value)
+        self.assertEqual(False, default)
+
+    def test_get_local_config(self):
+        local_config = self.cd.get_local_config()
+        self.assertEqual({}, local_config)
+        my_config = { "item1": 2, "item2": 2.2, "item3": False, "item4": "asdf", "item5": [ "c", "d" ] }
+        self.cd.set_local_config(my_config)
+        self.assertEqual(my_config, self.cd.get_local_config())
+
+    def test_get_item_list(self):
+        name_list = self.cd.get_item_list()
+        self.assertEqual(['item1', 'item2', 'item3', 'item4', 'item5/', 'item6/'], name_list)
+        name_list = self.cd.get_item_list("", True)
+        self.assertEqual(['item1', 'item2', 'item3', 'item4', 'item5/', 'item6/value1', 'item6/value2'], name_list)
+        name_list = self.cd.get_item_list("item6", False)
+        self.assertEqual(['item6/value1', 'item6/value2'], name_list)
+
+    def test_get_full_config(self):
+        full_config = self.cd.get_full_config()
+        self.assertEqual({ "item1": 1, "item2": 1.1, "item3": True, "item4": "test", "item5/": ['a', 'b'], "item6/value1": 'default', 'item6/value2': None}, full_config)
+        my_config = { "item1": 2, "item2": 2.2, "item3": False, "item4": "asdf", "item5": [ "c", "d" ] }
+        self.cd.set_local_config(my_config)
+        full_config = self.cd.get_full_config()
+        self.assertEqual({ "item1": 2, "item2": 2.2, "item3": False, "item4": "asdf", "item5/": [ "c", "d" ], "item6/value1": 'default', 'item6/value2': None}, full_config)
+
+class TestMultiConfigData(unittest.TestCase):
+    def setUp(self):
+        if 'CONFIG_TESTDATA_PATH' in os.environ:
+            self.data_path = os.environ['CONFIG_TESTDATA_PATH']
+        else:
+            self.data_path = "../../../testdata"
+        self.mcd = MultiConfigData()
+        
+    def test_init(self):
+        self.assertEqual({}, self.mcd._specifications)
+        self.assertEqual({}, self.mcd._current_config)
+        self.assertEqual({}, self.mcd._local_changes)
+
+    def test_set_specification(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
+        self.mcd.set_specification(module_spec)
+        self.assert_(module_spec.get_module_name() in self.mcd._specifications)
+        self.assertEquals(module_spec, self.mcd._specifications[module_spec.get_module_name()])
+
+    def test_get_module_spec(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
+        self.mcd.set_specification(module_spec)
+        module_spec2 = self.mcd.get_module_spec(module_spec.get_module_name())
+        self.assertEqual(module_spec, module_spec2)
+        module_spec3 = self.mcd.get_module_spec("no_such_module")
+        self.assertEqual(None, module_spec3)
+
+    def test_find_spec_part(self):
+        spec_part = self.mcd.find_spec_part("Spec2/item1")
+        self.assertEqual(None, spec_part)
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        spec_part = self.mcd.find_spec_part("Spec2/item1")
+        self.assertEqual({'item_name': 'item1', 'item_type': 'integer', 'item_optional': False, 'item_default': 1, }, spec_part)
+
+    def test_get_local_changes(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        local_changes = self.mcd.get_local_changes()
+        self.assertEqual({}, local_changes)
+        self.mcd.set_value("Spec2/item1", 2)
+        local_changes = self.mcd.get_local_changes()
+        self.assertEqual({"Spec2": { "item1": 2}}, local_changes)
+        
+
+    def test_clear_local_changes(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        self.mcd.set_value("Spec2/item1", 2)
+        self.mcd.clear_local_changes()
+        local_changes = self.mcd.get_local_changes()
+        self.assertEqual({}, local_changes)
+        pass
+
+    def test_get_local_value(self):
+        value = self.mcd.get_local_value("Spec2/item1")
+        self.assertEqual(None, value)
+        self.mcd.set_value("Spec2/item1", 2)
+        value = self.mcd.get_local_value("Spec2/item1")
+        self.assertEqual(2, value)
+
+    def test_get_current_value(self):
+        value = self.mcd.get_current_value("Spec2/item1")
+        self.assertEqual(None, value)
+        self.mcd._current_config = { "Spec2": { "item1": 3 } }
+        value = self.mcd.get_current_value("Spec2/item1")
+        self.assertEqual(3, value)
+        pass
+
+    def test_get_default_value(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        value = self.mcd.get_default_value("Spec2/item1")
+        self.assertEqual(1, value)
+        value = self.mcd.get_default_value("Spec2/item6/value1")
+        self.assertEqual('default', value)
+        value = self.mcd.get_default_value("Spec2/item6/value2")
+        self.assertEqual(None, value)
+        value = self.mcd.get_default_value("Spec2/no_such_item/asdf")
+        self.assertEqual(None, value)
+
+    def test_get_value(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        self.mcd.set_value("Spec2/item1", 2)
+        value,status = self.mcd.get_value("Spec2/item1")
+        self.assertEqual(2, value)
+        self.assertEqual(MultiConfigData.LOCAL, status)
+        value,status = self.mcd.get_value("Spec2/item2")
+        self.assertEqual(1.1, value)
+        self.assertEqual(MultiConfigData.DEFAULT, status)
+        self.mcd._current_config = { "Spec2": { "item3": False } }
+        value,status = self.mcd.get_value("Spec2/item3")
+        self.assertEqual(False, value)
+        self.assertEqual(MultiConfigData.CURRENT, status)
+        value,status = self.mcd.get_value("Spec2/no_such_item")
+        self.assertEqual(None, value)
+        self.assertEqual(MultiConfigData.NONE, status)
+
+    def test_get_value_maps(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        maps = self.mcd.get_value_maps()
+        self.assertEqual([{'default': False, 'type': 'module', 'name': 'Spec2', 'value': None, 'modified': False}], maps)
+
+    def test_set_value(self):
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        self.mcd.set_value("Spec2/item1", 2)
+        self.assertRaises(isc.cc.data.DataTypeError, self.mcd.set_value, "Spec2/item1", "asdf")
+        self.mcd.set_value("Spec2/no_such_item", 4)
+
+    def test_get_config_item_list(self):
+        config_items = self.mcd.get_config_item_list()
+        self.assertEqual([], config_items)
+        module_spec = isc.config.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
+        self.mcd.set_specification(module_spec)
+        config_items = self.mcd.get_config_item_list()
+        self.assertEqual(['Spec2'], config_items)
+        config_items = self.mcd.get_config_item_list("Spec2")
+        self.assertEqual(['Spec2/item1', 'Spec2/item2', 'Spec2/item3', 'Spec2/item4', 'Spec2/item5/', 'Spec2/item6/'], config_items)
+        config_items = self.mcd.get_config_item_list("Spec2", True)
+        self.assertEqual(['Spec2/item1', 'Spec2/item2', 'Spec2/item3', 'Spec2/item4', 'Spec2/item5/', 'Spec2/item6/value1', 'Spec2/item6/value2'], config_items)
 
 if __name__ == '__main__':
     unittest.main()