Repository: ESMCI/cime

Issue:
xmlchange allows setting invalid values
For at least some variables, `xmlchange` allows setting invalid values. Here is a simple reproducer:
```
./create_test SMS.f19_f19.A.hobart_gnu --output-root /scratch/cluster/${USER}/Atest --test-root /scratch/cluster/${USER}/Atest --test-id foo --wait
cd /scratch/cluster/${USER}/Atest/SMS.f19_f19.A.hobart_gnu.foo
./xmlchange PIO_TYPENAME_ATM=eieio
./xmlquery PIO_TYPENAME_ATM
```
This may be related to the use of the `component` property since `valid_values` is enforced for most variables. 
xmlchange does not enforce --file option
When using the `--file` option to `xmlchange`, incorrect or bogus files are not trapped. For instance:
`./xmlchange --file env_test.xml INFO_DBUG=3`
sets `INFO_DBUG` in env_run.xml instead of env_test.xml. Worse:
`./xmlchange --file blarg INFO_DBUG=2`
also sets the value in env_run.xml.


Discussion:

Resolving patch:
diff --git a/scripts/lib/CIME/XML/env_mach_specific.py b/scripts/lib/CIME/XML/env_mach_specific.py
--- a/scripts/lib/CIME/XML/env_mach_specific.py
+++ b/scripts/lib/CIME/XML/env_mach_specific.py
@@ -13,7 +13,7 @@
 # get_type) otherwise need to implement own functions and make GenericXML parent class
 class EnvMachSpecific(EnvBase):
     # pylint: disable=unused-argument
-    def __init__(self, caseroot, infile="env_mach_specific.xml",
+    def __init__(self, caseroot=None, infile="env_mach_specific.xml",
                  components=None, unit_testing=False):
         """
         initialize an object interface to file env_mach_specific.xml in the case directory
diff --git a/scripts/lib/CIME/XML/generic_xml.py b/scripts/lib/CIME/XML/generic_xml.py
--- a/scripts/lib/CIME/XML/generic_xml.py
+++ b/scripts/lib/CIME/XML/generic_xml.py
@@ -294,4 +294,5 @@ def get_raw_record(self, root=None):
             expect(False, "Could not write file %s, xml formatting error '%s'" % (self.filename, e))
         return xmlstr
 
-
+    def get_id(self):
+        return self.root.get("id")
diff --git a/scripts/lib/CIME/case.py b/scripts/lib/CIME/case.py
--- a/scripts/lib/CIME/case.py
+++ b/scripts/lib/CIME/case.py
@@ -30,6 +30,7 @@
 from CIME.XML.env_run               import EnvRun
 from CIME.XML.env_archive           import EnvArchive
 from CIME.XML.env_batch             import EnvBatch
+from CIME.XML.generic_xml           import GenericXML
 from CIME.user_mod_support          import apply_user_mods
 from CIME.case_setup import case_setup
 from CIME.aprun import get_aprun_cmd_for_case
@@ -200,13 +201,14 @@ def get_case_root(self):
         """Returns the root directory for this case."""
         return self._caseroot
 
-    def get_env(self, short_name):
+    def get_env(self, short_name, allow_missing=False):
         full_name = "env_%s.xml" % (short_name)
         for env_file in self._files:
             if os.path.basename(env_file.filename) == full_name:
                 return env_file
-
-        expect(False, "Could not find object for %s in case"%full_name)
+        if allow_missing:
+            return None
+        expect(False,"Could not find object for %s in case"%full_name)
 
     def copy(self, newcasename, newcaseroot, newcimeroot=None, newsrcroot=None):
         newcase = deepcopy(self)
@@ -281,7 +283,8 @@ def get_value(self, item, attribute=None, resolved=True, subgroup=None):
                 if resolved and type(result) is str:
                     result = self.get_resolved_value(result)
                     vtype = env_file.get_type_info(item)
-                    result = convert_to_type(result, vtype, item)
+                    if vtype is not None:
+                        result = convert_to_type(result, vtype, item)
                 return result
 
         for env_file in self._env_generic_files:
@@ -370,6 +373,7 @@ def set_value(self, item, value, subgroup=None, ignore_type=False):
         if item == "CASEROOT":
             self._caseroot = value
         result = None
+
         for env_file in self._env_entryid_files:
             result = env_file.set_value(item, value, subgroup, ignore_type)
             if (result is not None):
@@ -488,7 +492,7 @@ def __iter__(self):
                 else:
                     yield key, val
 
-    def _set_comp_classes(self, comp_classes):
+    def set_comp_classes(self, comp_classes):
         self._component_classes = comp_classes
         for env_file in self._env_entryid_files:
             env_file.set_components(comp_classes)
@@ -517,7 +521,7 @@ def _get_component_config_data(self, files):
 
         # loop over all elements of both component_classes and components - and get config_component_file for
         # for each component
-        self._set_comp_classes(drv_comp.get_valid_model_components())
+        self.set_comp_classes(drv_comp.get_valid_model_components())
 
         if len(self._component_classes) > len(self._components):
             self._components.append('sesp')
@@ -1192,3 +1196,50 @@ def _check_testlists(self, compset_alias, grid_name, files):
             expect(False, "\nThis compset and grid combination is untested in CESM.  "
                    "Override this warning with the --run-unsupported option to create_newcase.",
                    error_prefix="STOP: ")
+
+    def set_file(self, xmlfile):
+        """
+        force the case object to consider only xmlfile
+        """
+        expect(os.path.isfile(xmlfile), "Could not find file %s"%xmlfile)
+
+        gfile = GenericXML(infile=xmlfile)
+        ftype = gfile.get_id()
+
+        logger.warn("setting case file to %s"%xmlfile)
+        new_env_file = None
+        for env_file in self._env_entryid_files:
+            if os.path.basename(env_file.filename) == ftype:
+                if ftype == "env_run.xml":
+                    new_env_file = EnvRun(infile=xmlfile)
+                elif ftype == "env_build.xml":
+                    new_env_file = EnvBuild(infile=xmlfile)
+                elif ftype == "env_case.xml":
+                    new_env_file = EnvCase(infile=xmlfile)
+                elif ftype == "env_mach_pes.xml":
+                    new_env_file = EnvMachPes(infile=xmlfile)
+                elif ftype == "env_batch.xml":
+                    new_env_file = EnvBatch(infile=xmlfile)
+                elif ftype == "env_test.xml":
+                    new_env_file = EnvTest(infile=xmlfile)
+            if new_env_file is not None:
+                self._env_entryid_files = []
+                self._env_generic_files = []
+                self._env_entryid_files.append(new_env_file)
+                break
+        if new_env_file is None:
+            for env_file in self._env_generic_files:
+                if os.path.basename(env_file.filename) == ftype:
+                    if ftype == "env_archive.xml":
+                        new_env_file = EnvArchive(infile=xmlfile)
+                    elif ftype == "env_mach_specific.xml":
+                        new_env_file = EnvMachSpecific(infile=xmlfile)
+                    else:
+                        expect(False, "No match found for file type %s"%ftype)
+                if new_env_file is not None:
+                    self._env_entryid_files = []
+                    self._env_generic_files = []
+                    self._env_generic_files.append(new_env_file)
+                    break
+
+        self._files = self._env_entryid_files + self._env_generic_files


Test patch:
diff --git a/scripts/lib/CIME/XML/env_test.py b/scripts/lib/CIME/XML/env_test.py
--- a/scripts/lib/CIME/XML/env_test.py
+++ b/scripts/lib/CIME/XML/env_test.py
@@ -103,3 +103,22 @@ def cleanupnode(self, node):
         if dnode is not None:
             node.remove(dnode)
         return node
+
+    def set_value(self, vid, value, subgroup=None, ignore_type=False):
+        """
+        check if vid is in test section of file
+        """
+        newval = EnvBase.set_value(self, vid, value, subgroup, ignore_type)
+        if newval is None:
+            tnode = self.get_optional_node("test")
+            if tnode is not None:
+                newval = self.set_element_text(vid, value, root=tnode)
+        return newval
+
+    def get_value(self, vid, attribute=None, resolved=True, subgroup=None):
+        value = EnvBase.get_value(self, vid, attribute, resolved, subgroup)
+        if value is None:
+            tnode = self.get_optional_node("test")
+            if tnode is not None:
+                value = self.get_element_text(vid, root=tnode)
+        return value