Repository: ESMValGroup/ESMValCore

Issue:
If custom location for custom CMOR tables is used, default location is ignored
**Describe the bug**
If a custom location for custom CMOR tables is used via `custom: cmor_path: ...` in `config-developer`, the default location `esmvalcore/cmor/tables/custom` is ignored. To avoid copying all existing tables to a custom location, this should be fixed.

I will open a PR for this shortly.

@FranziskaWinterstein

Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2279?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Comparison is base [(`1cf02d3`)](https://app.codecov.io/gh/ESMValGroup/ESMValCore/commit/1cf02d385ff9088471ad5406601ac1a6f95408d7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) 93.48% compared to head [(`2bfd70a`)](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2279?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup) 93.48%.


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2279   +/-   ##
=======================================
  Coverage   93.48%   93.48%           
=======================================
  Files         238      238           
  Lines       12948    12948           
=======================================
  Hits        12105    12105           
  Misses        843      843           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/ESMValGroup/ESMValCore/pull/2279?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ESMValGroup).

schlunma: @FranziskaWinterstein could you try if this solves your problem with the custom tables? Thanks!!
valeriupredoi: BTW @schlunma I think this closes this issue too https://github.com/ESMValGroup/ESMValCore/issues/1388 - may be worth keeping that tool issue open until this gets merged and either an RC or full new Core version is out and about :beer:
schlunma: > BTW @schlunma I think this closes this issue too #1388 🍺

Nah I don't think so, this PR only changes the behavior if a custom location for the custom location is specified, #1388 also appears with our default location for the custom tables (I think 😁).
valeriupredoi: ah well, worth the try, am in Xmas cleaning mode :grin:
FranziskaWinterstein: @schlunma Thank you for taking care of this. However, I have difficulties in executing my recipe. I was able to reproduce the former error, but with your changes the following happens: `ERROR:esmvalcore._main:Program terminated abnormally, see stack trace below for more information:
Traceback (most recent call last):
  File "/home/b/b309109/ESMValCore/esmvalcore/_main.py", line 518, in run
    fire.Fire(ESMValTool())
  File "/home/b/b309109/ESMValCore/esmvalcore/_main.py", line 319, in __init__
    self.__setattr__(entry_point.name, entry_point.load()())
  File "/work/bd1132/b309109/conda_envs/envs/esmvaltool/lib/python3.10/importlib/metadata/__init__.py", line 171, in load
    module = import_module(match.group('module'))
  File "/work/bd1132/b309109/conda_envs/envs/esmvaltool/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/home/b/b309109/ESMValTool/esmvaltool/cmorizers/data/cmorizer.py", line 17, in <module>
    from esmvalcore._task import write_ncl_settings
  File "/home/b/b309109/ESMValCore/esmvalcore/_task.py", line 24, in <module>
    from ._citation import _write_citation_files
  File "/home/b/b309109/ESMValCore/esmvalcore/_citation.py", line 10, in <module>
    from .config._diagnostics import DIAGNOSTICS
  File "/home/b/b309109/ESMValCore/esmvalcore/config/__init__.py", line 11, in <module>
    from ._config_object import CFG, Config, Session
  File "/home/b/b309109/ESMValCore/esmvalcore/config/_config_object.py", line 242, in <module>
    CFG = Config._load_user_config(USER_CONFIG, raise_exception=False)
  File "/home/b/b309109/ESMValCore/esmvalcore/config/_config_object.py", line 70, in _load_user_config
    new.update(mapping)
  File "/work/bd1132/b309109/conda_envs/envs/esmvaltool/lib/python3.10/_collections_abc.py", line 999, in update
    self[key] = other[key]
  File "/home/b/b309109/ESMValCore/esmvalcore/config/_validated_config.py", line 65, in __setitem__
    raise InvalidConfigParameter(
esmvalcore.exceptions.InvalidConfigParameter: `offline` is not a valid config parameter.
`
schlunma: It looks like you are using the removed option `offline` in one of your config files (could also be the default one at `~/.esmvaltool/config-user.yml`). `offline` has been deprecated in v2.8 and removed just now for v2.10 (https://github.com/ESMValGroup/ESMValCore/pull/2213). The new option is called `search_esgf` (details [here](https://github.com/ESMValGroup/ESMValCore/pull/1935)).
schlunma: See https://github.com/ESMValGroup/ESMValCore/issues/2280
FranziskaWinterstein: Okay, after solving all my config issues, the behaviour concerning the custom tables is as I would expect it, i.e. favouring the external folder over the internal only if duplicate tables are defined. Thank you!

Patch:
diff --git a/doc/quickstart/configure.rst b/doc/quickstart/configure.rst
index 4caf7e794..c14bcb6ea 100644
--- a/doc/quickstart/configure.rst
+++ b/doc/quickstart/configure.rst
@@ -683,7 +683,8 @@ related to CMOR table settings available:
   extended with variables from the :ref:`custom_cmor_tables` (by default loaded
   from the ``esmvalcore/cmor/tables/custom`` directory) and it is possible to
   use variables with a ``mip`` which is different from the MIP table in which
-  they are defined.
+  they are defined. Note that this option is always enabled for
+  :ref:`derived <Variable derivation>` variables.
 * ``cmor_path``: path to the CMOR table.
   Relative paths are with respect to `esmvalcore/cmor/tables`_.
   Defaults to the value provided in ``cmor_type`` written in lower case.
@@ -698,11 +699,18 @@ Custom CMOR tables
 
 As mentioned in the previous section, the CMOR tables of projects that use
 ``cmor_strict: false`` will be extended with custom CMOR tables.
-By default, these are loaded from `esmvalcore/cmor/tables/custom
+For derived variables (the ones with ``derive: true`` in the recipe), the
+custom CMOR tables will always be considered.
+By default, these custom tables are loaded from `esmvalcore/cmor/tables/custom
 <https://github.com/ESMValGroup/ESMValCore/tree/main/esmvalcore/cmor/tables/custom>`_.
 However, by using the special project ``custom`` in the
 ``config-developer.yml`` file with the option ``cmor_path``, a custom location
-for these custom CMOR tables can be specified:
+for these custom CMOR tables can be specified.
+In this case, the default custom tables are extended with those entries from
+the custom location (in case of duplication, the custom location tables take
+precedence).
+
+Example:
 
 .. code-block:: yaml
 
@@ -742,11 +750,10 @@ Example for the file ``CMOR_asr.dat``:
    !----------------------------------
    !
 
-It is also possible to use a special coordinates file ``CMOR_coordinates.dat``.
-If this is not present in the custom directory, the one from the default
-directory (`esmvalcore/cmor/tables/custom/CMOR_coordinates.dat
-<https://github.com/ESMValGroup/ESMValCore/tree/main/esmvalcore/cmor/tables/custom/CMOR_coordinates.dat>`_)
-is used.
+It is also possible to use a special coordinates file ``CMOR_coordinates.dat``,
+which will extend the entries from the default one
+(`esmvalcore/cmor/tables/custom/CMOR_coordinates.dat
+<https://github.com/ESMValGroup/ESMValCore/tree/main/esmvalcore/cmor/tables/custom/CMOR_coordinates.dat>`_).
 
 
 .. _filterwarnings_config-developer:
diff --git a/doc/recipe/preprocessor.rst b/doc/recipe/preprocessor.rst
index 7ad4bdbce..fdac9ec7d 100644
--- a/doc/recipe/preprocessor.rst
+++ b/doc/recipe/preprocessor.rst
@@ -201,13 +201,15 @@ the ozone 3D field. In this case, a derivation function is provided to
 vertically integrate the ozone and obtain total column ozone for direct
 comparison with the observations.
 
-To contribute a new derived variable, it is also necessary to define a name for
-it and to provide the corresponding CMOR table. This is to guarantee the proper
-metadata definition is attached to the derived data. Such custom CMOR tables
-are collected as part of the `ESMValCore package
-<https://github.com/ESMValGroup/ESMValCore>`_. By default, the variable
-derivation will be applied only if the variable is not already available in the
-input data, but the derivation can be forced by setting the appropriate flag.
+The tool will also look in other ``mip`` tables for the same ``project`` to find
+the definition of derived variables. To contribute a completely new derived
+variable, it is necessary to define a name for it and to provide the
+corresponding CMOR table. This is to guarantee the proper metadata definition
+is attached to the derived data. Such custom CMOR tables are collected as part
+of the `ESMValCore package <https://github.com/ESMValGroup/ESMValCore/tree/main/esmvalcore/cmor/tables/custom>`_.
+By default, the variable derivation will be applied only if the variable is not
+already available in the input data, but the derivation can be forced by
+setting the ``force_derivation`` flag.
 
 .. code-block:: yaml
 
diff --git a/esmvalcore/_recipe/recipe.py b/esmvalcore/_recipe/recipe.py
index 1613f6cf6..d245880fa 100644
--- a/esmvalcore/_recipe/recipe.py
+++ b/esmvalcore/_recipe/recipe.py
@@ -250,25 +250,42 @@ def _add_to_download_list(dataset):
 
 
 def _schedule_for_download(datasets):
-    """Schedule files for download and show the list of files in the log."""
+    """Schedule files for download."""
     for dataset in datasets:
         _add_to_download_list(dataset)
         for supplementary_ds in dataset.supplementaries:
             _add_to_download_list(supplementary_ds)
 
-        files = list(dataset.files)
-        for supplementary_ds in dataset.supplementaries:
-            files.extend(supplementary_ds.files)
+
+def _log_input_files(datasets: Iterable[Dataset]) -> None:
+    """Show list of files in log (including supplementaries)."""
+    for dataset in datasets:
+        # Only log supplementary variables if present
+        supplementary_files_str = ""
+        if dataset.supplementaries:
+            for sup_ds in dataset.supplementaries:
+                supplementary_files_str += (
+                    f"\nwith files for supplementary variable "
+                    f"{sup_ds['short_name']}:\n{_get_files_str(sup_ds)}"
+                )
 
         logger.debug(
-            "Using input files for variable %s of dataset %s:\n%s",
+            "Using input files for variable %s of dataset %s:\n%s%s",
             dataset.facets['short_name'],
-            dataset.facets['alias'].replace('_', ' '),
-            '\n'.join(f'{f} (will be downloaded)' if not f.exists() else str(f)
-                      for f in files),
+            dataset.facets['alias'].replace('_', ' '),  # type: ignore
+            _get_files_str(dataset),
+            supplementary_files_str
         )
 
 
+def _get_files_str(dataset: Dataset) -> str:
+    """Get nice string representation of all files of a dataset."""
+    return '\n'.join(
+        f'  {f}' if f.exists()  # type: ignore
+        else f'  {f} (will be downloaded)' for f in dataset.files
+    )
+
+
 def _check_input_files(input_datasets: Iterable[Dataset]) -> set[str]:
     """Check that the required input files are available."""
     missing = set()
@@ -517,6 +534,7 @@ def _get_preprocessor_products(
         _set_version(dataset, input_datasets)
         USED_DATASETS.append(dataset)
         _schedule_for_download(input_datasets)
+        _log_input_files(input_datasets)
         logger.info("Found input files for %s", dataset.summary(shorten=True))
 
         filename = _get_output_file(
diff --git a/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py b/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py
index fdc349d08..7cdf0e903 100644
--- a/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py
+++ b/esmvalcore/cmor/_fixes/cmip6/gfdl_cm4.py
@@ -68,30 +68,24 @@ class Tas(Fix):
         return cubes
 
 
-class Uas(Fix):
-    """Fixes for uas."""
+class Tasmin(Tas):
+    """Fixes for tasmin."""
 
-    def fix_metadata(self, cubes):
-        """
-        Add height (10m) coordinate.
 
-        Parameters
-        ----------
-        cubes : iris.cube.CubeList
-            Input cubes.
+class Tasmax(Tas):
+    """Fixes for tasmax."""
 
-        Returns
-        -------
-        iris.cube.CubeList
 
-        """
-        cube = self.get_cube_from_list(cubes)
-        add_scalar_height_coord(cube, 10.0)
-        return cubes
+class SfcWind(Tas):
+    """Fixes for sfcWind."""
 
 
-class Vas(Fix):
-    """Fixes for vas."""
+class Hurs(Tas):
+    """Fixes for hurs."""
+
+
+class Uas(Fix):
+    """Fixes for uas."""
 
     def fix_metadata(self, cubes):
         """
@@ -112,6 +106,10 @@ class Vas(Fix):
         return cubes
 
 
+class Vas(Uas):
+    """Fixes for vas."""
+
+
 Omon = BaseOmon
 
 
diff --git a/esmvalcore/cmor/_fixes/cmip6/kiost_esm.py b/esmvalcore/cmor/_fixes/cmip6/kiost_esm.py
index 77ab4805e..e06922a0a 100644
--- a/esmvalcore/cmor/_fixes/cmip6/kiost_esm.py
+++ b/esmvalcore/cmor/_fixes/cmip6/kiost_esm.py
@@ -35,6 +35,14 @@ class Huss(Tas):
     """Fixes for huss."""
 
 
+class Tasmin(Tas):
+    """Fixes for tasmin."""
+
+
+class Tasmax(Tas):
+    """Fixes for tasmax."""
+
+
 class SfcWind(Fix):
     """Fixes for sfcWind."""
 
diff --git a/esmvalcore/cmor/table.py b/esmvalcore/cmor/table.py
index ebce5431a..6eba32426 100644
--- a/esmvalcore/cmor/table.py
+++ b/esmvalcore/cmor/table.py
@@ -262,7 +262,7 @@ class InfoBase():
         self,
         table_name: str,
         short_name: str,
-        derived: Optional[bool] = False,
+        derived: bool = False,
     ) -> VariableInfo | None:
         """Search and return the variable information.
 
@@ -296,11 +296,11 @@ class InfoBase():
                     pass
 
         # If that didn't work, look in all tables (i.e., other MIPs) if
-        # cmor_strict=False
-        var_info = self._look_in_all_tables(alt_names_list)
+        # cmor_strict=False or derived=True
+        var_info = self._look_in_all_tables(derived, alt_names_list)
 
-        # If that didn' work either, look in default table if cmor_strict=False
-        # or derived=True
+        # If that didn't work either, look in default table if
+        # cmor_strict=False or derived=True
         if not var_info:
             var_info = self._look_in_default(derived, alt_names_list,
                                              table_name)
@@ -324,10 +324,10 @@ class InfoBase():
                     break
         return var_info
 
-    def _look_in_all_tables(self, alt_names_list):
+    def _look_in_all_tables(self, derived, alt_names_list):
         """Look for variable in all tables."""
         var_info = None
-        if not self.strict:
+        if (not self.strict or derived):
             for alt_names in alt_names_list:
                 var_info = self._look_all_tables(alt_names)
                 if var_info:
@@ -988,51 +988,52 @@ class CustomInfo(CMIP5Info):
 
     Parameters
     ----------
-    cmor_tables_path: str or None
+    cmor_tables_path:
         Full path to the table or name for the table if it is present in
-        ESMValTool repository
-    """
-
-    def __init__(self, cmor_tables_path=None):
-        cwd = os.path.dirname(os.path.realpath(__file__))
-        default_cmor_folder = os.path.join(cwd, 'tables', 'custom')
+        ESMValTool repository. If ``None``, use default tables from
+        `esmvalcore/cmor/tables/custom`.
 
-        # Get custom location of CMOR tables if possible
-        if cmor_tables_path is None:
-            self._cmor_folder = default_cmor_folder
-        else:
-            self._cmor_folder = self._get_cmor_path(cmor_tables_path)
-        if not os.path.isdir(self._cmor_folder):
-            raise ValueError(f"Custom CMOR tables path {self._cmor_folder} is "
-                             f"not a directory")
+    """
 
+    def __init__(self, cmor_tables_path: Optional[str | Path] = None) -> None:
+        """Initialize class member."""
+        self.coords = {}
         self.tables = {}
-        self.var_to_freq = {}
+        self.var_to_freq: dict[str, dict] = {}
         table = TableInfo()
         table.name = 'custom'
         self.tables[table.name] = table
 
-        # Try to read coordinates from custom location, use default location if
-        # not possible
-        coordinates_file = os.path.join(
-            self._cmor_folder,
-            'CMOR_coordinates.dat',
-        )
-        if os.path.isfile(coordinates_file):
-            self._coordinates_file = coordinates_file
+        # First, read default custom tables from repository
+        self._cmor_folder = self._get_cmor_path('custom')
+        self._read_table_dir(self._cmor_folder)
+
+        # Second, if given, update default tables with user-defined custom
+        # tables
+        if cmor_tables_path is not None:
+            self._user_table_folder = self._get_cmor_path(cmor_tables_path)
+            if not os.path.isdir(self._user_table_folder):
+                raise ValueError(
+                    f"Custom CMOR tables path {self._user_table_folder} is "
+                    f"not a directory"
+                )
+            self._read_table_dir(self._user_table_folder)
         else:
-            self._coordinates_file = os.path.join(
-                default_cmor_folder,
-                'CMOR_coordinates.dat',
-            )
+            self._user_table_folder = None
 
-        self.coords = {}
-        self._read_table_file(self._coordinates_file, self.tables['custom'])
-        for dat_file in glob.glob(os.path.join(self._cmor_folder, '*.dat')):
-            if dat_file == self._coordinates_file:
+    def _read_table_dir(self, table_dir: str) -> None:
+        """Read CMOR tables from directory."""
+        # If present, read coordinates
+        coordinates_file = os.path.join(table_dir, 'CMOR_coordinates.dat')
+        if os.path.isfile(coordinates_file):
+            self._read_table_file(coordinates_file)
+
+        # Read other variables
+        for dat_file in glob.glob(os.path.join(table_dir, '*.dat')):
+            if dat_file == coordinates_file:
                 continue
             try:
-                self._read_table_file(dat_file, self.tables['custom'])
+                self._read_table_file(dat_file)
             except Exception:
                 msg = f"Exception raised when loading {dat_file}"
                 # Logger may not be ready at this stage
@@ -1042,29 +1043,40 @@ class CustomInfo(CMIP5Info):
                     print(msg)
                 raise
 
-    def get_variable(self, table, short_name, derived=False):
+    def get_variable(
+        self,
+        table: str,
+        short_name: str,
+        derived: bool = False
+    ) -> VariableInfo | None:
         """Search and return the variable info.
 
         Parameters
         ----------
-        table: str
-            Table name
-        short_name: str
-            Variable's short name
-        derived: bool, optional
+        table:
+            Table name. Ignored for custom tables.
+        short_name:
+            Variable's short name.
+        derived:
             Variable is derived. Info retrieval for derived variables always
-            look on the default tables if variable is not find in the
-            requested table
+            looks on the default tables if variable is not found in the
+            requested table. Ignored for custom tables.
 
         Returns
         -------
-        VariableInfo
-            Return the VariableInfo object for the requested variable if
-            found, returns None if not
+        VariableInfo | None
+            `VariableInfo` object for the requested variable if found, returns
+            None if not.
+
         """
         return self.tables['custom'].get(short_name, None)
 
-    def _read_table_file(self, table_file, table=None):
+    def _read_table_file(
+        self,
+        table_file: str,
+        _: Optional[TableInfo] = None,
+    ) -> None:
+        """Read a single table file."""
         with open(table_file, 'r', encoding='utf-8') as self._current_table:
             self._read_line()
             while True:
@@ -1079,7 +1091,9 @@ class CustomInfo(CMIP5Info):
                     self.coords[value] = self._read_coordinate(value)
                     continue
                 elif key == 'variable_entry':
-                    table[value] = self._read_variable(value, '')
+                    self.tables['custom'][value] = self._read_variable(
+                        value, ''
+                    )
                     continue
                 if not self._read_line():
                     return
diff --git a/esmvalcore/preprocessor/_area.py b/esmvalcore/preprocessor/_area.py
index ae78526a2..145b87ec3 100644
--- a/esmvalcore/preprocessor/_area.py
+++ b/esmvalcore/preprocessor/_area.py
@@ -84,7 +84,6 @@ def extract_region(
             latitude=(start_latitude, end_latitude),
             ignore_bounds=True,
         )
-        region_subset = region_subset.intersection(longitude=(0., 360.))
     else:
         region_subset = _extract_irregular_region(
             cube,


Test patch:
diff --git a/tests/integration/cmor/_fixes/cmip6/test_gfdl_cm4.py b/tests/integration/cmor/_fixes/cmip6/test_gfdl_cm4.py
index f28310c6c..46b4424f5 100644
--- a/tests/integration/cmor/_fixes/cmip6/test_gfdl_cm4.py
+++ b/tests/integration/cmor/_fixes/cmip6/test_gfdl_cm4.py
@@ -10,11 +10,14 @@ from esmvalcore.cmor._fixes.cmip6.gfdl_cm4 import (
     Fgco2,
     Omon,
     Siconc,
+    Tas,
+    Uas,
 )
 from esmvalcore.cmor._fixes.common import SiconcFixScalarCoord
 from esmvalcore.cmor._fixes.fix import GenericFix
 from esmvalcore.cmor.fix import Fix
 from esmvalcore.cmor.table import get_var_info
+from cf_units import Unit
 
 
 def test_get_cl_fix():
@@ -116,3 +119,103 @@ def test_get_siconc_fix():
 def test_siconc_fix():
     """Test fix for ``siconc``."""
     assert Siconc is SiconcFixScalarCoord
+
+
+@pytest.fixture
+def tas_cubes():
+    correct_lat_coord = iris.coords.DimCoord([0.0],
+                                             var_name='lat',
+                                             standard_name='latitude')
+    wrong_lat_coord = iris.coords.DimCoord([0.0],
+                                           var_name='latitudeCoord',
+                                           standard_name='latitude')
+    correct_lon_coord = iris.coords.DimCoord([0.0],
+                                             var_name='lon',
+                                             standard_name='longitude')
+    wrong_lon_coord = iris.coords.DimCoord([0.0],
+                                           var_name='longitudeCoord',
+                                           standard_name='longitude')
+    correct_cube = iris.cube.Cube([[2.0]],
+                                  var_name='tas',
+                                  dim_coords_and_dims=[(correct_lat_coord, 0),
+                                                       (correct_lon_coord, 1)])
+    wrong_cube = iris.cube.Cube([[2.0]],
+                                var_name='ta',
+                                dim_coords_and_dims=[(wrong_lat_coord, 0),
+                                                     (wrong_lon_coord, 1)])
+    scalar_cube = iris.cube.Cube(0.0, var_name='ps')
+    return iris.cube.CubeList([correct_cube, wrong_cube, scalar_cube])
+
+
+def test_get_tas_fix():
+    fixes = Fix.get_fixes('CMIP6', 'GFDL-CM4', 'day', 'tas')
+    assert Tas(None) in fixes
+
+
+def test_tas_fix_metadata(tas_cubes):
+    for cube in tas_cubes:
+        with pytest.raises(iris.exceptions.CoordinateNotFoundError):
+            cube.coord('height')
+    height_coord = iris.coords.AuxCoord(2.0,
+                                        var_name='height',
+                                        standard_name='height',
+                                        long_name='height',
+                                        units=Unit('m'),
+                                        attributes={'positive': 'up'})
+    vardef = get_var_info('CMIP6', 'day', 'tas')
+    fix = Tas(vardef)
+
+    out_cubes = fix.fix_metadata(tas_cubes)
+    assert out_cubes[0].var_name == 'tas'
+    coord = out_cubes[0].coord('height')
+    assert coord == height_coord
+
+
+@pytest.fixture
+def uas_cubes():
+    correct_lat_coord = iris.coords.DimCoord([0.0],
+                                             var_name='lat',
+                                             standard_name='latitude')
+    wrong_lat_coord = iris.coords.DimCoord([0.0],
+                                           var_name='latitudeCoord',
+                                           standard_name='latitude')
+    correct_lon_coord = iris.coords.DimCoord([0.0],
+                                             var_name='lon',
+                                             standard_name='longitude')
+    wrong_lon_coord = iris.coords.DimCoord([0.0],
+                                           var_name='longitudeCoord',
+                                           standard_name='longitude')
+    correct_cube = iris.cube.Cube([[10.0]],
+                                  var_name='uas',
+                                  dim_coords_and_dims=[(correct_lat_coord, 0),
+                                                       (correct_lon_coord, 1)])
+    wrong_cube = iris.cube.Cube([[10.0]],
+                                var_name='ua',
+                                dim_coords_and_dims=[(wrong_lat_coord, 0),
+                                                     (wrong_lon_coord, 1)])
+    scalar_cube = iris.cube.Cube(0.0, var_name='ps')
+    return iris.cube.CubeList([correct_cube, wrong_cube, scalar_cube])
+
+
+def test_get_uas_fix():
+    fixes = Fix.get_fixes('CMIP6', 'GFDL-CM4', 'day', 'uas')
+    assert Uas(None) in fixes
+
+
+def test_uas_fix_metadata(uas_cubes):
+    for cube in uas_cubes:
+        with pytest.raises(iris.exceptions.CoordinateNotFoundError):
+            cube.coord('height')
+    height_coord = iris.coords.AuxCoord(10.0,
+                                        var_name='height',
+                                        standard_name='height',
+                                        long_name='height',
+                                        units=Unit('m'),
+                                        attributes={'positive': 'up'})
+    vardef = get_var_info('CMIP6', 'day', 'uas')
+    fix = Uas(vardef)
+
+    out_cubes = fix.fix_metadata(uas_cubes)
+    assert out_cubes[0].var_name == 'uas'
+    coord = out_cubes[0].coord('height')
+    assert coord == height_coord
diff --git a/tests/integration/cmor/test_read_cmor_tables.py b/tests/integration/cmor/test_read_cmor_tables.py
index 32a472b31..a77b9b294 100644
--- a/tests/integration/cmor/test_read_cmor_tables.py
+++ b/tests/integration/cmor/test_read_cmor_tables.py
@@ -1,4 +1,5 @@
 from pathlib import Path
+from textwrap import dedent
 
 import pytest
 import yaml
@@ -7,17 +8,6 @@ from esmvalcore.cmor.table import CMOR_TABLES
 from esmvalcore.cmor.table import __file__ as root
 from esmvalcore.cmor.table import read_cmor_tables
 
-CUSTOM_CFG_DEVELOPER = {
-    'custom': {'cmor_path': Path(root).parent / 'tables' / 'custom'},
-    'CMIP6': {
-        'cmor_strict': True,
-        'input_dir': {'default': '/'},
-        'input_file': '*.nc',
-        'output_file': 'out.nc',
-        'cmor_type': 'CMIP6',
-    },
-}
-
 
 def test_read_cmor_tables_raiser():
     """Test func raiser."""
@@ -52,12 +42,108 @@ def test_read_cmor_tables():
     assert Path(table._cmor_folder) == table_path / 'obs4mips' / 'Tables'
     assert table.strict is False
 
+    project = 'custom'
+    table = CMOR_TABLES[project]
+    assert Path(table._cmor_folder) == table_path / 'custom'
+    assert table._user_table_folder is None
+    assert table.coords
+    assert table.tables['custom']
+
+
+CMOR_NEWVAR_ENTRY = dedent(
+    """
+    !============
+    variable_entry:    newvarfortesting
+    !============
+    modeling_realm:    atmos
+    !----------------------------------
+    ! Variable attributes:
+    !----------------------------------
+    standard_name:
+    units:             kg s m A
+    cell_methods:      time: mean
+    cell_measures:     area: areacella
+    long_name:         Custom Variable for Testing
+    !----------------------------------
+    ! Additional variable information:
+    !----------------------------------
+    dimensions:        longitude latitude time
+    type:              real
+    positive:          up
+    !----------------------------------
+    !
+    """
+)
+CMOR_NETCRE_ENTRY = dedent(
+    """
+    !============
+    variable_entry:    netcre
+    !============
+    modeling_realm:    atmos
+    !----------------------------------
+    ! Variable attributes:
+    !----------------------------------
+    standard_name:     air_temperature  ! for testing
+    units:             K                ! for testing
+    cell_methods:      time: mean
+    cell_measures:     area: areacella
+    long_name:         This is New      ! for testing
+    !----------------------------------
+    ! Additional variable information:
+    !----------------------------------
+    dimensions:        longitude latitude time
+    type:              real
+    positive:          up
+    !----------------------------------
+    !
+    """
+)
+CMOR_NEWCOORD_ENTRY = dedent(
+    """
+    !============
+    axis_entry: newcoordfortesting
+    !============
+    !----------------------------------
+    ! Axis attributes:
+    !----------------------------------
+    standard_name:
+    units:            kg
+    axis:             Y             ! X, Y, Z, T (default: undeclared)
+    long_name:        Custom Coordinate for Testing
+    !----------------------------------
+    ! Additional axis information:
+    !----------------------------------
+    out_name:         newcoordfortesting
+    valid_min:        -90.0
+    valid_max:        90.0
+    stored_direction: increasing
+    type:             double
+    must_have_bounds: yes
+    !----------------------------------
+    !
+    """
+)
+
 
 def test_read_custom_cmor_tables(tmp_path):
     """Test reading of custom CMOR tables."""
+    (tmp_path / 'CMOR_newvarfortesting.dat').write_text(CMOR_NEWVAR_ENTRY)
+    (tmp_path / 'CMOR_netcre.dat').write_text(CMOR_NETCRE_ENTRY)
+    (tmp_path / 'CMOR_coordinates.dat').write_text(CMOR_NEWCOORD_ENTRY)
+
+    custom_cfg_developer = {
+        'custom': {'cmor_path': str(tmp_path)},
+        'CMIP6': {
+            'cmor_strict': True,
+            'input_dir': {'default': '/'},
+            'input_file': '*.nc',
+            'output_file': 'out.nc',
+            'cmor_type': 'CMIP6',
+        },
+    }
     cfg_file = tmp_path / 'config-developer.yml'
     with cfg_file.open('w', encoding='utf-8') as file:
-        yaml.safe_dump(CUSTOM_CFG_DEVELOPER, file)
+        yaml.safe_dump(custom_cfg_developer, file)
 
     read_cmor_tables(cfg_file)
 
@@ -66,10 +152,23 @@ def test_read_custom_cmor_tables(tmp_path):
     assert 'custom' in CMOR_TABLES
 
     custom_table = CMOR_TABLES['custom']
-    assert (Path(custom_table._cmor_folder) ==
-            Path(root).parent / 'tables' / 'custom')
-    assert (Path(custom_table._coordinates_file) ==
-            Path(root).parent / 'tables' / 'custom' / 'CMOR_coordinates.dat')
+    assert (
+        custom_table._cmor_folder ==
+        str(Path(root).parent / 'tables' / 'custom')
+    )
+    assert custom_table._user_table_folder == str(tmp_path)
+
+    # Make sure that default tables have been read
+    assert 'alb' in custom_table.tables['custom']
+    assert 'latitude' in custom_table.coords
+
+    # Make sure that custom tables have been read
+    assert 'newvarfortesting' in custom_table.tables['custom']
+    assert 'newcoordfortesting' in custom_table.coords
+    netcre = custom_table.get_variable('custom', 'netcre')
+    assert netcre.standard_name == 'air_temperature'
+    assert netcre.units == 'K'
+    assert netcre.long_name == 'This is New'
 
     cmip6_table = CMOR_TABLES['CMIP6']
     assert cmip6_table.default is custom_table
diff --git a/tests/integration/cmor/test_table.py b/tests/integration/cmor/test_table.py
index 8b8d63e57..385869bc5 100644
--- a/tests/integration/cmor/test_table.py
+++ b/tests/integration/cmor/test_table.py
@@ -105,6 +105,11 @@ class TestCMIP6Info(unittest.TestCase):
         var = self.variables_info.get_variable('SImon', 'sic')
         self.assertEqual(var.short_name, 'siconc')
 
+    def test_get_variable_derived(self):
+        """Test that derived variable are looked up from other MIP tables."""
+        var = self.variables_info.get_variable('3hr', 'sfcWind', derived=True)
+        self.assertEqual(var.short_name, 'sfcWind')
+
     def test_get_variable_from_custom(self):
         """Get a variable from default."""
         self.variables_info.strict = False
@@ -411,32 +416,23 @@ class TestCustomInfo(unittest.TestCase):
             'tables',
             'custom',
         )
-        expected_coordinate_file = os.path.join(
-            os.path.dirname(esmvalcore.cmor.__file__),
-            'tables',
-            'custom',
-            'CMOR_coordinates.dat',
-        )
         self.assertEqual(custom_info._cmor_folder, expected_cmor_folder)
-        self.assertEqual(custom_info._coordinates_file,
-                         expected_coordinate_file)
+        self.assertTrue(custom_info.tables['custom'])
+        self.assertTrue(custom_info.coords)
 
     def test_custom_tables_location(self):
         """Test constructor with custom tables location."""
         cmor_path = os.path.dirname(os.path.realpath(esmvalcore.cmor.__file__))
+        default_cmor_tables_path = os.path.join(cmor_path, 'tables', 'custom')
         cmor_tables_path = os.path.join(cmor_path, 'tables', 'cmip5')
         cmor_tables_path = os.path.abspath(cmor_tables_path)
+
         custom_info = CustomInfo(cmor_tables_path)
-        self.assertEqual(custom_info._cmor_folder, cmor_tables_path)
 
-        expected_coordinate_file = os.path.join(
-            os.path.dirname(esmvalcore.cmor.__file__),
-            'tables',
-            'custom',
-            'CMOR_coordinates.dat',
-        )
-        self.assertEqual(custom_info._coordinates_file,
-                         expected_coordinate_file)
+        self.assertEqual(custom_info._cmor_folder, default_cmor_tables_path)
+        self.assertEqual(custom_info._user_table_folder, cmor_tables_path)
+        self.assertTrue(custom_info.tables['custom'])
+        self.assertTrue(custom_info.coords)
 
     def test_custom_tables_invalid_location(self):
         """Test constructor with invalid custom tables location."""
diff --git a/tests/unit/preprocessor/_area/test_area.py b/tests/unit/preprocessor/_area/test_area.py
index a74ac8ba0..3939437f3 100644
--- a/tests/unit/preprocessor/_area/test_area.py
+++ b/tests/unit/preprocessor/_area/test_area.py
@@ -770,7 +770,7 @@ def test_crop_cube_with_ne_file(ne_ocean_shapefile):
         result = _crop_cube(cube, *geometries.bounds, cmor_coords=False)
         result = (result.coord("latitude").points[-1],
                   result.coord("longitude").points[-1])
-        expected = (89., 359.)
+        expected = (89., 179.)
         np.testing.assert_allclose(result, expected)