Repository: ioos/compliance-checker
Pull request #929 (2022-05-20 14:58:17)

Merge in CI changes from develop branch


Patch:
diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml
deleted file mode 100644
index 7233479..0000000
--- a/.github/workflows/pre-commit.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-name: pre-commit
-
-on:
-  pull_request:
-  push:
-    branches: [master]
-
-jobs:
-  pre-commit:
-    runs-on: ubuntu-latest
-    steps:
-    - uses: actions/checkout@v2
-    - uses: actions/setup-python@v2
-    - uses: pre-commit/action@v2.0.0
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2d9087f..0cfde21 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.1.0
+  rev: v4.2.0
   hooks:
     - id: trailing-whitespace
       exclude: compliance_checker/tests/data
@@ -34,7 +34,7 @@ repos:
     - id: seed-isort-config
 
 - repo: https://github.com/psf/black
-  rev: 21.12b0
+  rev: 22.3.0
   hooks:
   - id: black
     language_version: python3
@@ -44,7 +44,7 @@ repos:
   hooks:
     - id: codespell
       args:
-        - --ignore-words-list=degreee,varn,poit,uint,sur,herat,claus
+        - --ignore-words-list=degreeE,degreee,varn,poit,uint,sur,herat
       exclude: >
           (?x)^(
               .*\.xml|
@@ -52,3 +52,14 @@ repos:
               .*_version.py|
               .*versioneer.py
           )$
+
+ci:
+    autofix_commit_msg: |
+        [pre-commit.ci] auto fixes from pre-commit.com hooks
+
+        for more information, see https://pre-commit.ci
+    autofix_prs: false
+    autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
+    autoupdate_schedule: monthly
+    skip: []
+    submodules: false
diff --git a/README.md b/README.md
index ab2a0e3..8fb54fc 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,9 @@ for completeness and community standard compliance of local or remote
 file standards. The python module can be used as a command-line tool or as a
 library that can be integrated into other software.
 
+## Python 2.7 Deprecation Notice
+As of release 4.3.0, the __Compliance Checker no longer officially supports Python 2.7__.
+
 A [web-based version](https://data.ioos.us/compliance/index.html) of the Compliance
 Checker was developed to enable a broader audience and improve accessibility for the
 checker. With the web version, providers can simply provide a link or upload their
@@ -435,14 +438,20 @@ compliance-checker -t ncei-trajectory-profile-orthogonal -v ~/data/sample-trajec
 compliance-checker -t ncei-grid -f json -o ~/Documents/sample_grid_report.json ~/Documents/sample_grid_report.nc
 ```
 
-## Disclaimer
+## Contributors
 
-The objective of the IOOS Compliance Checker is to check your file against our interpretation of select dataset metadata standards to use as a guideline in generating compliant files. The compliance checker should not be considered the authoritative source on whether your file is 100% "compliant". Instead, we recommend that users use the results as a guide to work towards compliance.
+- [Dave Foster](https://github.com/daf) &lt;dave@axiomdatascience.com&gt;
+- [Dan Maher](https://github.com/danieljmaher) &lt;daniel.maher@gdit.com&gt;
+- [Luke Campbell](https://github.com/lukecampbell) &lt;luke@axiomdatascience.com&gt;
+- [Kyle Wilcox](https://github.com/kwilcox) &lt;kyle@axiomdatascience.com&gt;
+- [Ben Adams](https://github.com/benjwadams) &lt;ben.adams@rpsgroup.com&gt;
+- [Bob Fratantonio](https://github.com/bobfrat) &lt;robert.fratantonio@rpsgroup.com&gt;
+- [Dalton Kell](https://github.com/daltonkell) &lt;dalton.kell@rpsgroup.com&gt;
 
+And many more testers!
 
-## Miscellaneous/Acknowledgements
+Portions of the CF checker are based on Michael Decker's work, http://repositories.iek.fz-juelich.de/hg/CFchecker/
 
-### Contributors
-![GitHub Contributors Image](https://contrib.rocks/image?repo=ioos/compliance-checker)
+## Disclaimer
 
-Portions of the CF checker are based on Michael Decker's work, http://repositories.iek.fz-juelich.de/hg/CFchecker/
+The objective of the IOOS Compliance Checker is to check your file against our interpretation of select dataset metadata standards to use as a guideline in generating compliant files. The compliance checker should not be considered the authoritative source on whether your file is 100% "compliant". Instead, we recommend that users use the results as a guide to work towards compliance.
diff --git a/cchecker.py b/cchecker.py
index 243fc73..7297daa 100755
--- a/cchecker.py
+++ b/cchecker.py
@@ -112,7 +112,7 @@ def main():
                                     will skip medium and low.  "L" will show
                                     both high and medium priority issues, while
                                     skipping low priority issues.  Cannot be
-                                    used with `-i`/`--include-checks` option.
+                                    used with `-i`/`--include` option.
                                     """
         ),
         action="append",
diff --git a/compliance_checker/acdd.py b/compliance_checker/acdd.py
index 7f490df..f19903d 100644
--- a/compliance_checker/acdd.py
+++ b/compliance_checker/acdd.py
@@ -769,7 +769,7 @@ class ACDD1_3Check(ACDDNCCheck):
 
         :param netCDF4.Dataset ds: An open netCDF dataset
         """
-        if not hasattr(ds, "metadata_link"):
+        if not hasattr(ds, u"metadata_link"):
             return
         msgs = []
         meta_link = getattr(ds, "metadata_link")
@@ -784,14 +784,14 @@ class ACDD1_3Check(ACDDNCCheck):
 
         :param netCDF4.Dataset ds: An open netCDF dataset
         """
-        if not hasattr(ds, "id"):
+        if not hasattr(ds, u"id"):
             return
-        if " " in getattr(ds, "id"):
+        if " " in getattr(ds, u"id"):
             return Result(
                 BaseCheck.MEDIUM,
                 False,
                 "no_blanks_in_id",
-                msgs=["There should be no blanks in the id field"],
+                msgs=[u"There should be no blanks in the id field"],
             )
         else:
             return Result(BaseCheck.MEDIUM, True, "no_blanks_in_id", msgs=[])
@@ -829,7 +829,7 @@ class ACDD1_3Check(ACDDNCCheck):
             }
             if ctype not in valid_ctypes:
                 msgs.append(
-                    'coverage_content_type "%s" not in %s' % (variable, sorted(valid_ctypes))
+                    'coverage_content_type in "%s"' % (variable, sorted(valid_ctypes))
                 )
                 results.append(
                     Result(
diff --git a/compliance_checker/base.py b/compliance_checker/base.py
index 6bebcd8..8992b2e 100644
--- a/compliance_checker/base.py
+++ b/compliance_checker/base.py
@@ -7,6 +7,7 @@ import csv
 import itertools
 import pprint
 import re
+import sys
 import warnings
 
 from collections import defaultdict
@@ -158,6 +159,7 @@ class BaseCheck(object):
 
         Automatically run when running a CheckSuite. Define this method in your Checker class.
         """
+        pass
 
     def __init__(self, options=None):
         self._defined_results = defaultdict(lambda: defaultdict(dict))
diff --git a/compliance_checker/cf/__init__.py b/compliance_checker/cf/__init__.py
index 573f950..a7757b2 100644
--- a/compliance_checker/cf/__init__.py
+++ b/compliance_checker/cf/__init__.py
@@ -2,8 +2,7 @@ from compliance_checker.cf.appendix_d import (
     dimless_vertical_coordinates_1_6,
     dimless_vertical_coordinates_1_7,
 )
-from compliance_checker.cf.cf import CF1_6Check, CF1_7Check
-from compliance_checker.cf import util
+from compliance_checker.cf.cf import CF1_6Check, CF1_7Check, util
 
 
 __all__ = [
diff --git a/compliance_checker/cf/appendix_c.py b/compliance_checker/cf/appendix_c.py
deleted file mode 100644
index 344e262..0000000
--- a/compliance_checker/cf/appendix_c.py
+++ /dev/null
@@ -1,12 +0,0 @@
-"""Appendix C - Standard Name modifiers"""
-
-# Dict of standard name modifiers with values for units.  "u" indicates to use
-# the same units as the standard name canonical units, "1" is unitless for
-# observation counts, and None is used for status_flag, which expects units
-# not to be present
-valid_modifiers = {
-    "detection_minimum": "u",
-    "number_of_observations": "1",
-    "standard_error": "u",
-    "status_flag": None,
-}
diff --git a/compliance_checker/cf/appendix_d.py b/compliance_checker/cf/appendix_d.py
index 5e80ca5..b71dab9 100644
--- a/compliance_checker/cf/appendix_d.py
+++ b/compliance_checker/cf/appendix_d.py
@@ -27,28 +27,11 @@ is omitted from the formula_terms attribute should be assumed to be zero.
 # vertical coordinates only have one computed_standard_name, but some have
 # multiple acceptable values.
 ocean_computed_standard_names = {
-    "altitude": {
-        "zlev": "altitude",
-        "eta": "sea_surface_height_above_geoid",
-        "depth": "sea_floor_depth_below_geoid",
-    },
-    "height_above_geopotential_datum": {
-        "zlev": "height_above_geopotential_datum",
-        "eta": "sea_surface_height_above_geopotential_datum",
-        "depth": "sea_floor_depth_below_geopotential_datum",
-    },
-    "height_above_reference_ellipsoid": {
-        "zlev": "height_above_reference_ellipsoid",
-        "eta": "sea_surface_height_above_reference_ellipsoid",
-        "depth": "sea_floor_depth_below_reference_ellipsoid",
-    },
-    "height_above_mean_sea_level": {
-        "zlev": "height_above_mean_sea_level",
-        "eta": "sea_surface_height_above_mean_sea_level",
-        "depth": "sea_floor_depth_below_mean_sea_level",
-    },
+    "altitude",
+    "height_above_geopotential_datum",
+    "height_above_reference_ellipsoid",
+    "height_above_mean_sea_level",
 }
-
 dimless_vertical_coordinates_1_6 = {  # only for CF-1.6
     "atmosphere_ln_pressure_coordinate": ({"p0", "lev"}, {"air_pressure"}),
     "atmosphere_sigma_coordinate": ({"sigma", "ps", "ptop"}, {"air_pressure"}),
diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py
index 747e3b2..83d7a9c 100644
--- a/compliance_checker/cf/cf.py
+++ b/compliance_checker/cf/cf.py
@@ -1,17 +1,30 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
+import logging
+import os
+import sqlite3
+import sys
 
-from compliance_checker import cfutil  # noqa: F401
-from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx  # noqa: F401
-from compliance_checker.cf import util  # noqa: F401
-from compliance_checker.cf.appendix_d import (  # noqa: F401
+from collections import OrderedDict, defaultdict
+from functools import wraps
+from warnings import warn
+
+import numpy as np
+import pyproj
+import regex
+
+from cf_units import Unit
+
+from compliance_checker import cfutil
+from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
+from compliance_checker.cf import util
+from compliance_checker.cf.appendix_d import (
     dimless_vertical_coordinates_1_6,
     dimless_vertical_coordinates_1_7,
     no_missing_terms,
 )
-
-from compliance_checker.cf.appendix_e import cell_methods16, cell_methods17  # noqa: F401
-from compliance_checker.cf.appendix_f import (  # noqa: F401
+from compliance_checker.cf.appendix_e import cell_methods16, cell_methods17
+from compliance_checker.cf.appendix_f import (
     ellipsoid_names17,
     grid_mapping_attr_types16,
     grid_mapping_attr_types17,
@@ -22,6 +35,6 @@ from compliance_checker.cf.appendix_f import (  # noqa: F401
 )
 
 # Version specific checkers organized in other modules
-from compliance_checker.cf.cf_1_6 import CF1_6Check  # noqa: F401
-from compliance_checker.cf.cf_1_7 import CF1_7Check  # noqa: F401
-from compliance_checker.cf.cf_1_8 import CF1_8Check  # noqa: F401
+from compliance_checker.cf.cf_1_6 import CF1_6Check
+from compliance_checker.cf.cf_1_7 import CF1_7Check
+from compliance_checker.cf.cf_1_8 import CF1_8Check
diff --git a/compliance_checker/cf/cf_1_6.py b/compliance_checker/cf/cf_1_6.py
index fb87b1d..bfe9367 100644
--- a/compliance_checker/cf/cf_1_6.py
+++ b/compliance_checker/cf/cf_1_6.py
@@ -1,6 +1,10 @@
 import logging
+import os
+import sys
 
-from collections import defaultdict
+from collections import OrderedDict, defaultdict
+from functools import wraps
+from warnings import warn
 
 import numpy as np
 import regex
@@ -8,24 +12,17 @@ import regex
 from cf_units import Unit
 
 from compliance_checker import cfutil
-from compliance_checker.base import BaseCheck, Result, TestCtx
+from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
 from compliance_checker.cf import util
-from compliance_checker.cf.appendix_c import valid_modifiers
-from compliance_checker.cf.appendix_d import (
-    dimless_vertical_coordinates_1_6,
-)
+from compliance_checker.cf.appendix_d import (dimless_vertical_coordinates_1_6,
+                                              no_missing_terms)
 from compliance_checker.cf.appendix_e import cell_methods16
-from compliance_checker.cf.appendix_f import (
-    grid_mapping_attr_types16,
-    grid_mapping_dict16,
-)
+from compliance_checker.cf.appendix_f import (grid_mapping_attr_types16,
+                                              grid_mapping_dict16)
 
 from compliance_checker.cf.cf_base import CFNCCheck, appendix_a_base
-import difflib
-
 logger = logging.getLogger(__name__)
 
-
 class CF1_6Check(CFNCCheck):
     """CF-1.6-specific implementation of CFBaseCheck; supports checking
     netCDF datasets.
@@ -40,7 +37,6 @@ class CF1_6Check(CFNCCheck):
     _cc_url = "http://cfconventions.org/cf-conventions/v1.6.0/cf-conventions.html"
     _cc_display_headers = {3: "Errors", 2: "Warnings", 1: "Info"}
     appendix_a = appendix_a_base
-    appendix_d_parametric_coords = dimless_vertical_coordinates_1_6
 
     def __init__(self, options=None):  # initialize with parent methods and data
         super(CF1_6Check, self).__init__(options)
@@ -52,15 +48,6 @@ class CF1_6Check(CFNCCheck):
     ###############################################################################
     # Chapter 2: NetCDF Files and Components
     ###############################################################################
-    def check_filename(self, ds):
-        """Checks that the filename ends with .nc"""
-        # IMPLEMENTS CONFORMANCE 2.1
-        filename_suffix = TestCtx(BaseCheck.HIGH, self.section_titles["2.1"])
-        filename_suffix.assert_true(
-            ds.filepath().endswith("nc"),
-            f'Dataset path {ds.filepath} must end with ".nc"',
-        )
-        return filename_suffix.to_result()
 
     def check_data_types(self, ds):
         """
@@ -73,7 +60,6 @@ class CF1_6Check(CFNCCheck):
         :param netCDF4.Dataset ds: An open netCDF dataset
         :rtype: compliance_checker.base.Result
         """
-        # IMPLEMENTS CONFORMANCE 2.2
         fails = []
         total = len(ds.variables)
 
@@ -159,14 +145,14 @@ class CF1_6Check(CFNCCheck):
             val = (
                 att.dtype == variable.dtype
             ) or (  # will short-circuit or if first condition is true
-                isinstance(att, (np.float32, np.float64, float))
-                and variable.dtype
-                in (np.byte, np.short, np.int16, np.int, np.int32, int)
+                isinstance(att.dtype, (np.float, np.double, float))
+                and isinstance(variable.dtype, (np.byte, np.short, np.int, int))
             )
         if not val:
             msgs.append(error_msg)
 
-        return Result(BaseCheck.MEDIUM, val, self.section_titles["8.1"], msgs)
+        return Result(BaseCheck.MEDIUM, val, self.section_titles["8.1"],
+                      msgs)
 
     def check_add_offset_scale_factor_type(self, ds):
         """
@@ -184,24 +170,6 @@ class CF1_6Check(CFNCCheck):
             scale_factor=lambda x: x is not None
         )
 
-        both = set(add_offset_vars).intersection(scale_factor_vars)
-        both_msgs = []
-        for both_var in sorted(both, key=lambda var: var.name):
-            if both_var.scale_factor.dtype != both_var.add_offset.dtype:
-                both_msgs.append(
-                    "When both scale_factor and add_offset "
-                    f"are supplied for variable {both_var.name}, "
-                    "they must have the same type"
-                )
-        results.append(
-            Result(
-                BaseCheck.MEDIUM,
-                not bool(both_msgs),
-                self.section_titles["8.1"],
-                both_msgs,
-            )
-        )
-
         for _att_vars_tup in (
             ("add_offset", add_offset_vars),
             ("scale_factor", scale_factor_vars),
@@ -244,7 +212,6 @@ class CF1_6Check(CFNCCheck):
 
         rname = regex.compile("^[A-Za-z][A-Za-z0-9_]*$")
 
-        # IMPLEMENTATION CONFORMANCE 2.3 REQUIRED
         for name, variable in ds.variables.items():
             variable_naming.assert_true(
                 rname.match(name) is not None,
@@ -307,7 +274,6 @@ class CF1_6Check(CFNCCheck):
         total = len(ds.variables)
         names = defaultdict(int)
 
-        # IMPLEMENTATION CONFORMANCE 2.3 RECOMMENDED
         for k in ds.variables:
             names[k.lower()] += 1
 
@@ -341,7 +307,6 @@ class CF1_6Check(CFNCCheck):
             for d in v.dimensions:
                 dims[d] += 1
 
-            # IMPLEMENTATION CONFORMANCE 2.4 REQUIRED
             for dimension, count in dims.items():
                 if count > 1:
                     fails.append(
@@ -584,27 +549,13 @@ class CF1_6Check(CFNCCheck):
         coordinate_variables = self._find_coord_vars(ds)
         auxiliary_coordinates = self._find_aux_coord_vars(ds)
         geophysical_variables = self._find_geophysical_vars(ds)
-        modifier_variables = cfutil._find_standard_name_modifier_variables(ds)
         forecast_variables = cfutil.get_forecast_metadata_variables(ds)
 
-        dimless_vert = {
-            var.name
-            for var in ds.get_variables_by_attributes(
-                standard_name=lambda s: s in self.appendix_d_parametric_coords
-            )
-            if not hasattr(var, "units")
-        }
-        # check anything remaining that has units
-        # unit_containing =
-        unit_required_variables = (
-            set(
-                coordinate_variables
-                + auxiliary_coordinates
-                + geophysical_variables
-                + forecast_variables
-                + modifier_variables
-            )  # standard names with modifiers require proper units, *except* for flags, where they should not be present
-            - dimless_vert
+        unit_required_variables = set(
+            coordinate_variables
+            + auxiliary_coordinates
+            + geophysical_variables
+            + forecast_variables
         )
 
         for name in unit_required_variables:
@@ -638,7 +589,7 @@ class CF1_6Check(CFNCCheck):
             units_attr_is_string = TestCtx(BaseCheck.MEDIUM, self.section_titles["3.1"])
 
             # side effects, but better than teasing out the individual result
-            if units is not None and units_attr_is_string.assert_true(
+            if units_attr_is_string.assert_true(
                 isinstance(units, str),
                 "units ({}) attribute of '{}' must be a string compatible with UDUNITS".format(
                     units, variable.name
@@ -649,7 +600,6 @@ class CF1_6Check(CFNCCheck):
             ret_val.append(units_attr_is_string.to_result())
 
             if isinstance(standard_name, str):
-                # CONFORMANCE 3.1 REQUIRED
                 valid_standard_units = self._check_valid_standard_units(ds, name)
                 ret_val.append(valid_standard_units)
 
@@ -670,7 +620,6 @@ class CF1_6Check(CFNCCheck):
         deprecated = ["level", "layer", "sigma_level"]
         variable = ds.variables[variable_name]
 
-        valid_units = TestCtx(BaseCheck.HIGH, self.section_titles["3.1"])
         units = getattr(variable, "units", None)
         standard_name_full = getattr(variable, "standard_name", None)
         standard_name, standard_name_modifier = self._split_standard_name(
@@ -679,41 +628,6 @@ class CF1_6Check(CFNCCheck):
         std_name_units_dimensionless = cfutil.is_dimensionless_standard_name(
             self._std_names._root, standard_name
         )
-
-        # 3) units are not deprecated
-        valid_units.assert_true(
-            units not in deprecated,
-            'units for {}, "{}" are deprecated by CF 1.6'.format(variable_name, units),
-        )
-        # 4/5) Modifiers, if present, have the appropriate units, or none for
-        #    status_flag
-        if standard_name_modifier is not None:
-            if standard_name_modifier not in valid_modifiers:
-                # standard name modifier warning given elsewhere
-                return valid_units.to_result()
-            else:
-                unit_type = valid_modifiers[standard_name_modifier]
-        # no modifiers, just check against standard name canonical_units
-        else:
-            unit_type = "u"
-
-        if unit_type == "u":
-            try:
-                reference = self._std_names[standard_name].canonical_units
-            # if standard name isn't found, there won't be an associated units
-            # but a standard name error will be raised elsewhere
-            except KeyError:
-                return valid_units.to_result()
-        elif unit_type == "1":
-            reference = "1"
-        elif unit_type is None:
-            valid_units.assert_true(
-                units is None,
-                f"units attribute for variable {variable_name} must be unset "
-                "when status_flag standard name modifier is set",
-            )
-            return valid_units.to_result()
-
         # Is this even in the database? also, if there is no standard_name,
         # there's no way to know if it is dimensionless.
         should_be_dimensionless = (
@@ -724,6 +638,7 @@ class CF1_6Check(CFNCCheck):
         )
 
         # 1) Units must exist
+        valid_units = TestCtx(BaseCheck.HIGH, self.section_titles["3.1"])
         valid_units.assert_true(
             should_be_dimensionless or units is not None,
             "units attribute is required for {} when variable is not a dimensionless quantity".format(
@@ -740,28 +655,11 @@ class CF1_6Check(CFNCCheck):
             "units attribute for {} needs to be a string".format(variable_name),
         )
 
-        try:
-            units_conv = Unit(units)
-        except ValueError:
-            valid_units.messages.append(
-                f'Unit string "{units}" is not recognized by UDUnits'
-            )
-            valid_units.out_of += 1
-            return valid_units
-        else:
-            valid_units.score += 1
-            valid_units.out_of += 1
-
-        # time and forecast_reference time have special unit handling rules
-        # that use time relative to a reference point, despite canonical units
-        # being expressed as "s"/seconds
-        if standard_name not in {"time", "forecast_reference_time"}:
-            valid_units.assert_true(
-                units_conv.is_convertible(Unit(reference)),
-                f'Units "{units}" for variable '
-                f"{variable_name} must be convertible to "
-                f'canonical units "{reference}"',
-            )
+        # 3) units are not deprecated
+        valid_units.assert_true(
+            units not in deprecated,
+            'units for {}, "{}" are deprecated by CF 1.6'.format(variable_name, units),
+        )
 
         return valid_units.to_result()
 
@@ -791,7 +689,7 @@ class CF1_6Check(CFNCCheck):
         valid_udunits = TestCtx(BaseCheck.HIGH, self.section_titles["3.1"])
         are_udunits = units is not None and util.units_known(units)
         valid_udunits.assert_true(
-            should_be_dimensionless or are_udunits or units is None,
+            should_be_dimensionless or are_udunits,
             'units for {}, "{}" are not recognized by UDUNITS'.format(
                 variable_name, units
             ),
@@ -818,39 +716,27 @@ class CF1_6Check(CFNCCheck):
             self._std_names._root, standard_name
         )
 
-        if std_name_units_dimensionless:
-            return valid_standard_units.to_result()
-
         standard_name, standard_name_modifier = self._split_standard_name(standard_name)
 
+        standard_entry = self._std_names.get(standard_name, None)
+        if standard_entry is not None:
+            canonical_units = standard_entry.canonical_units
+        else:
+            # Any unit comparisons with None returns False
+            canonical_units = None
+
         # Other standard_name modifiers have the same units as the
         # unmodified standard name or are not checked for units.
 
-        # number_of_observations is a special case which always must be units
-        # of "1"
         if standard_name_modifier == "number_of_observations":
-            valid_standard_units.out_of += 1
-            if units != "1":
-                err_msg = (
-                    f"When variable {variable_name} has a "
-                    "standard name modifier of number_of_observations, "
-                    "the specified units must be 1"
-                )
-                valid_standard_units.messages.append(err_msg)
-            else:
-                valid_standard_units.score += 1
-            # number_of_observations should short circuit and not continue
-            # on to further units checks
-            return valid_standard_units.to_result()
+            canonical_units = "1"
 
         # This section represents the different cases where simple udunits
         # comparison isn't comprehensive enough to determine if the units are
         # appropriate under CF
 
         # UDUnits accepts "s" as a unit of time but it should be <unit> since <epoch>
-        # TODO: forecast_reference_time.  Include upcoming merge.
-        # IMPLEMENTATION CONFORMANCE 4.4 REQUIRED 1/2
-        elif standard_name == "time":
+        if standard_name == "time":
             valid_standard_units.assert_true(
                 util.units_convertible(units, "seconds since 1970-01-01"),
                 "time must be in a valid units format <unit> since <epoch> "
@@ -862,7 +748,7 @@ class CF1_6Check(CFNCCheck):
             # degrees is allowed if using a transformed grid
             allowed_units = cfutil.VALID_LAT_UNITS | {"degrees"}
             valid_standard_units.assert_true(
-                (units.lower() if units is not None else None) in allowed_units,
+                units.lower() in allowed_units,
                 'variables defining latitude ("{}") must use degrees_north '
                 "or degrees if defining a transformed grid. Currently "
                 "{}".format(variable_name, units),
@@ -872,11 +758,21 @@ class CF1_6Check(CFNCCheck):
             # degrees is allowed if using a transformed grid
             allowed_units = cfutil.VALID_LON_UNITS | {"degrees"}
             valid_standard_units.assert_true(
-                (units.lower() if units is not None else None) in allowed_units,
+                units.lower() in allowed_units,
                 'variables defining longitude ("{}") must use degrees_east '
                 "or degrees if defining a transformed grid. Currently "
                 "{}".format(variable_name, units),
             )
+        # Standard Name table agrees the unit should be dimensionless
+        elif std_name_units_dimensionless:
+            valid_standard_units.assert_true(True, "")
+
+        elif canonical_units is not None:
+            valid_standard_units.assert_true(
+                util.units_convertible(canonical_units, units),
+                "units for variable {} must be convertible to {} "
+                "currently they are {}".format(variable_name, canonical_units, units),
+            )
 
         return valid_standard_units.to_result()
 
@@ -945,7 +841,6 @@ class CF1_6Check(CFNCCheck):
 
             # If standard_name is not defined but long_name is, don't continue
             # the check for this variable
-            # IMPLEMENTATION CONFORMANCE 3.3 REQUIRED 1, 2, 3 / 3
             if standard_name is not None:
                 standard_name_present = True
                 valid_std_name = TestCtx(BaseCheck.HIGH, self.section_titles["3.3"])
@@ -955,38 +850,35 @@ class CF1_6Check(CFNCCheck):
                         name
                     ),
                 )
-                valid_std_name.out_of += 1
-                if standard_name not in self._std_names:
-                    err_msg = "standard_name {} is not defined in Standard Name Table v{}.".format(
-                        standard_name or "undefined", self._std_names._version
-                    )
-                    close_matches = difflib.get_close_matches(
-                        standard_name, self._std_names
+                if isinstance(standard_name, str):
+                    valid_std_name.assert_true(
+                        standard_name in self._std_names,
+                        "standard_name {} is not defined in Standard Name Table v{}".format(
+                            standard_name or "undefined", self._std_names._version
+                        ),
                     )
-                    if close_matches:
-                        err_msg += f" Possible close match(es): {close_matches}"
-                    valid_std_name.messages.append(err_msg)
-                else:
-                    valid_std_name.score += 1
 
                 ret_val.append(valid_std_name.to_result())
 
                 # 2) optional - if modifiers, should be in table
                 if standard_name_modifier is not None:
                     valid_modifier = TestCtx(BaseCheck.HIGH, self.section_titles["3.3"])
+                    allowed = [
+                        "detection_minimum",
+                        "number_of_observations",
+                        "standard_error",
+                        "status_flag",
+                    ]
                     valid_modifier.assert_true(
-                        standard_name_modifier in valid_modifiers,
-                        'Standard name modifier "{}" for variable {} is not a valid modifier '
-                        "according to CF Appendix C".format(
-                            standard_name_modifier, name
-                        ),
+                        standard_name_modifier in allowed,
+                        "standard_name modifier {} for variable {} is not a valid modifier "
+                        "according to appendix C".format(standard_name_modifier, name),
                     )
 
                     ret_val.append(valid_modifier.to_result())
             else:
                 standard_name_present = False
 
-            # IMPLEMENTATION CONFORMANCE 3 RECOMMENDED
             long_or_std_name.assert_true(
                 long_name_present or standard_name_present,
                 "Attribute long_name or/and standard_name is highly recommended for variable {}".format(
@@ -1105,20 +997,14 @@ class CF1_6Check(CFNCCheck):
                 ret_val.append(valid_masks)
 
             if flag_values is not None and flag_masks is not None:
-                vals_arr = np.array(flag_values, ndmin=1)
-                masks_arr = np.array(flag_masks, ndmin=1)
-                # IMPLEMENTATION CONFORMANCE 3.5 RECOMMENDED 1/1
-                # If shapes aren't equal, we can't do proper elementwise
-                # comparison
-                if vals_arr.size != masks_arr.size:
-                    allv = False
-                else:
-                    allv = np.all(vals_arr & masks_arr == vals_arr)
+                allv = list(
+                    map(lambda a, b: a & b == a, list(zip(flag_values, flag_masks)))
+                )
 
-                allvr = Result(BaseCheck.MEDIUM, allv, self.section_titles["3.5"])
+                allvr = Result(BaseCheck.MEDIUM, all(allv), self.section_titles["3.5"])
                 if not allvr.value:
                     allvr.msgs = [
-                        "flag masks and flag values for '{}' combined don't equal flag values".format(
+                        "flag masks and flag values for '{}' combined don't equal flag value".format(
                             name
                         )
                     ]
@@ -1146,21 +1032,25 @@ class CF1_6Check(CFNCCheck):
         flag_meanings = getattr(variable, "flag_meanings", None)
         valid_values = TestCtx(BaseCheck.HIGH, self.section_titles["3.5"])
 
-        # IMPLEMENTATION CONFORMANCE 3.5 REQUIRED 2/8
+        # flag_values must be a list of values, not a string or anything else
         valid_values.assert_true(
-            hasattr(variable, "flag_meanings"),
-            f"Variable {variable.name} must have attribute flag_meanings "
-            "defined when flag_values attribute is present",
+            isinstance(flag_values, np.ndarray),
+            "{}'s flag_values must be an array of values not {}".format(
+                name, type(flag_values)
+            ),
         )
 
+        # We can't perform any more checks
+        if not isinstance(flag_values, np.ndarray):
+            return valid_values.to_result()
+
         # the flag values must be independent, no repeating values
-        flag_set = np.unique(flag_values)
+        flag_set = set(flag_values)
         valid_values.assert_true(
-            flag_set.size == np.array(flag_values).size,
+            len(flag_set) == len(flag_values),
             "{}'s flag_values must be independent and can not be repeated".format(name),
         )
 
-        # IMPLEMENTATION CONFORMANCE 3.5 REQUIRED 1/8
         # the data type for flag_values should be the same as the variable
         valid_values.assert_true(
             variable.dtype.type == flag_values.dtype.type,
@@ -1168,13 +1058,14 @@ class CF1_6Check(CFNCCheck):
             "".format(flag_values.dtype.type, name, variable.dtype.type),
         )
 
-        # IMPLEMENTATION CONFORMANCE 3.5 REQUIRED 4/8
         if isinstance(flag_meanings, str):
             flag_meanings = flag_meanings.split()
             valid_values.assert_true(
-                len(flag_meanings) == np.array(flag_values).size,
-                f"{name}'s flag_meanings and flag_values should have the same "
-                "number of elements.",
+                len(flag_meanings) == len(flag_values),
+                "{}'s flag_meanings and flag_values should have the same number ".format(
+                    name
+                )
+                + "of elements.",
             )
 
         return valid_values.to_result()
@@ -1195,13 +1086,23 @@ class CF1_6Check(CFNCCheck):
         variable = ds.variables[name]
 
         flag_masks = variable.flag_masks
-        flag_meanings = getattr(variable, "flag_meanings", None)
+        flag_meanings = getattr(ds, "flag_meanings", None)
 
         valid_masks = TestCtx(BaseCheck.HIGH, self.section_titles["3.5"])
 
+        valid_masks.assert_true(
+            isinstance(flag_masks, np.ndarray),
+            "{}'s flag_masks must be an array of values not {}".format(
+                name, type(flag_masks).__name__
+            ),
+        )
+
+        if not isinstance(flag_masks, np.ndarray):
+            return valid_masks.to_result()
+
         valid_masks.assert_true(
             variable.dtype.type == flag_masks.dtype.type,
-            "flag_masks ({}) must be the same data type as {} ({})"
+            "flag_masks ({}) mustbe the same data type as {} ({})"
             "".format(flag_masks.dtype.type, name, variable.dtype.type),
         )
 
@@ -1211,12 +1112,6 @@ class CF1_6Check(CFNCCheck):
             or np.issubdtype(variable.dtype, "b")
         )
 
-        valid_masks.assert_true(
-            0 not in np.array(flag_masks),
-            f"flag_masks for variable {variable.name} must "
-            "not contain zero as an element",
-        )
-
         valid_masks.assert_true(
             type_ok,
             "{}'s data type must be capable of bit-field expression".format(name),
@@ -1225,11 +1120,11 @@ class CF1_6Check(CFNCCheck):
         if isinstance(flag_meanings, str):
             flag_meanings = flag_meanings.split()
             valid_masks.assert_true(
-                # cast to array here as single element arrays are returned as
-                # scalars from netCDF4 Python
-                len(flag_meanings) == np.array(flag_masks).size,
-                f"{name} flag_meanings and flag_masks should have the same "
-                "number of elements.",
+                len(flag_meanings) == len(flag_masks),
+                "{} flag_meanings and flag_masks should have the same number ".format(
+                    name
+                )
+                + "of elements.",
             )
 
         return valid_masks.to_result()
@@ -1268,7 +1163,6 @@ class CF1_6Check(CFNCCheck):
             len(flag_meanings) > 0, "{}'s flag_meanings can't be empty".format(name)
         )
 
-        # IMPLEMENTATION CONFORMANCE REQUIRED 3.5 3/8
         flag_regx = regex.compile(r"^[0-9A-Za-z_\-.+@]+$")
         meanings = flag_meanings.split()
         for meaning in meanings:
@@ -1752,7 +1646,6 @@ class CF1_6Check(CFNCCheck):
                 ret_val.append(result)
                 continue
             # Correct and identifiable units
-            # TODO: year zero climatological time warning
             result = Result(BaseCheck.HIGH, True, self.section_titles["4.4"])
             ret_val.append(result)
             correct_units = util.units_temporal(variable.units)
@@ -1808,7 +1701,7 @@ class CF1_6Check(CFNCCheck):
         :rtype: list
         :return: List of results
         """
-        valid_calendars = {
+        valid_calendars = [
             "gregorian",
             "standard",
             "proleptic_gregorian",
@@ -1819,7 +1712,7 @@ class CF1_6Check(CFNCCheck):
             "360_day",
             "julian",
             "none",
-        }
+        ]
 
         ret_val = []
 
@@ -1829,78 +1722,24 @@ class CF1_6Check(CFNCCheck):
             calendar=lambda c: c is not None
         ):
             reasoning = None
-            standard_calendar = time_var.calendar in valid_calendars
+            valid_calendar = time_var.calendar in valid_calendars
+
+            if not valid_calendar:
+                reasoning = [
+                    "§4.4.1 Variable %s should have a valid calendar: '%s' is not a valid calendar"
+                    % (time_var.name, time_var.calendar)
+                ]
 
-            # if a nonstandard calendar, then leap_years and leap_months must
-            # must be present
-            if not standard_calendar:
-                result = self._check_leap_time(time_var)
             # passes if the calendar is valid, otherwise notify of invalid
             # calendar
-            else:
-                result = Result(
-                    BaseCheck.LOW, True, self.section_titles["4.4"], reasoning
-                )
 
+            result = Result(
+                BaseCheck.LOW, valid_calendar, self.section_titles["4.4"], reasoning
+            )
             ret_val.append(result)
 
         return ret_val
 
-    def _check_leap_time(self, time_variable):
-        """
-        Helper method to handle checking custom calendar leap time specifiations
-        """
-        leap_time = TestCtx(BaseCheck.HIGH, self.section_titles["4.4"])
-        leap_time.out_of = 1
-        # IMPLEMENTATION CONFORMANCE 4.4.1 REQUIRED 2, 3 / 5
-        if not hasattr(time_variable, "month_lengths") or not (
-            hasattr(time_variable.month_lengths, "dtype")
-            and np.issubdtype(time_variable.month_lengths.dtype, np.integer)
-            and time_variable.month_lengths.size == 12
-        ):
-            leap_time.messages.append(
-                f"For nonstandard calendar on variable {time_variable.name}, "
-                "attribute month_lengths must be supplied as a 12-element "
-                "integer array"
-            )
-            return leap_time.to_result()
-        # If leap years are included, then attributes leap_month and
-        # leap_year must be included.
-        has_leap_year = hasattr(time_variable, "leap_year")
-        # IMPLEMENTATION CONFORMANCE 4.4.1 REQUIRED 4,5/5
-        if hasattr(time_variable, "leap_month"):
-            leap_time.assert_true(
-                (
-                    np.isscalar(time_variable.leap_month)
-                    and hasattr(time_variable.leap_month, "dtype")
-                    and np.issubdtype(time_variable.leap_month.dtype, np.integer)
-                    and 1 <= time_variable.leap_month <= 12
-                ),
-                "When attribute leap_month is supplied for variable "
-                f"{time_variable.name}, the value must be a scalar integer "
-                "between 1 and 12",
-            )
-            # IMPLEMENTATION CONFORMANCE 4.4.1 RECOMMENDED 1/2
-            if not has_leap_year:
-                leap_time.out_of += 1
-                fail_message = (
-                    f"For time variable {time_variable.name}, "
-                    "attribute leap_year must be present if "
-                    "leap_month attribute is defined"
-                )
-                leap_time.messages.append(fail_message)
-
-        # IMPLEMENTATION CONFORMANCE 4.4.1 REQUIRED 5/5
-        if has_leap_year:
-            leap_time.assert_true(
-                np.isscalar(time_variable.leap_year)
-                and hasattr(time_variable.leap_year, "dtype"),
-                "When attribute leap_year is supplied for variable "
-                f"{time_variable.name}, the value must be a scalar "
-                "integer",
-            )
-        return leap_time.to_result()
-
     ###############################################################################
     # Chapter 5: Coordinate Systems
     ###############################################################################
@@ -1965,7 +1804,6 @@ class CF1_6Check(CFNCCheck):
                 if aux_coord not in ds.variables:
                     continue
 
-                # TODO CONFORMANCE: Partial implementation of labels
                 # §6.1 Allows for "labels" to be referenced as coordinates
                 if (
                     hasattr(ds.variables[aux_coord].dtype, "char")
@@ -1990,6 +1828,7 @@ class CF1_6Check(CFNCCheck):
     def check_duplicate_axis(self, ds):
         """
         Checks that no variable contains two coordinates defining the same
+        axis.
 
         Chapter 5 paragraph 6
 
@@ -2632,7 +2471,6 @@ class CF1_6Check(CFNCCheck):
             cell_measures=lambda c: c is not None
         )
         for var in variables:
-            # IMPLEMENTATION CONFORMANCE REQUIRED 1/2
             search_str = r"^(?:area|volume): (\w+)$"
             search_res = regex.search(search_str, var.cell_measures)
             if not search_res:
@@ -2708,7 +2546,6 @@ class CF1_6Check(CFNCCheck):
         """
 
         ret_val = []
-        # CONFORMANCE IMPLEMENTATION 7.3 1/3
         psep = regex.compile(
             r"(?P<vars>\w+: )+(?P<method>\w+) ?(?P<where>where (?P<wtypevar>\w+) "
             r"?(?P<over>over (?P<otypevar>\w+))?| ?)(?:\((?P<paren_contents>[^)]*)\))?"
@@ -2787,7 +2624,6 @@ class CF1_6Check(CFNCCheck):
         Checks that the spacing and/or comment info contained inside the
         parentheses in cell_methods is well-formed
         """
-        # IMPLEMENTATION CONFORMANCE REQUIRED 3/3 - comment/paren contents
         valid_info = TestCtx(BaseCheck.MEDIUM, self.section_titles["7.3"])
         # if there are no colons, this is a simple comment
         # TODO: are empty comments considered valid?
@@ -2924,7 +2760,7 @@ class CF1_6Check(CFNCCheck):
             "median",
         ]
 
-        # find any climatology axis variables; any variables which contain climatological stats will use
+        # find any climatology axies variables; any variables which contain climatological stats will use
         # these variables as coordinates
         clim_time_coord_vars = ds.get_variables_by_attributes(
             climatology=lambda s: s is not None
@@ -2933,60 +2769,32 @@ class CF1_6Check(CFNCCheck):
         # first, to determine whether or not we have a valid climatological time
         # coordinate variable, we need to make sure it has the attribute "climatology",
         # but not the attribute "bounds"
-
         for clim_coord_var in clim_time_coord_vars:
-            climatology_ctx = TestCtx(BaseCheck.MEDIUM, self.section_titles["7.3"])
             if hasattr(clim_coord_var, "bounds"):
-                climatology_ctx.out_of += 1
-                climatology_ctx.messages.append(
-                    f"Variable {clim_coord_var.name} has a climatology "
-                    "attribute and cannot also have a bounds attribute."
+                reasoning.append(
+                    "Variable {} has a climatology attribute and cannot also have a bounds attribute.".format(
+                        clim_coord_var.name
+                    )
                 )
                 result = Result(
                     BaseCheck.MEDIUM, False, (self.section_titles["7.4"]), reasoning
                 )
+                ret_val.append(result)
 
-            # IMPLEMENTATION CONFORMANCE 7.4 REQUIRED 2/6
             # make sure the climatology variable referenced actually exists
             elif clim_coord_var.climatology not in ds.variables:
-                climatology_ctx.out_of += 1
-                climatology_ctx.messages.append(
+                reasoning.append(
                     "Variable {} referenced in time's climatology attribute does not exist".format(
                         ds.variables["time"].climatology
                     )
                 )
-            else:
-                # IMPLEMENTATION CONFORMANCE 7.4 REQUIRED 4/6
-                clim_var = ds.variables[clim_coord_var.climatology]
-                if clim_var.dtype is str or not np.issubdtype(clim_var, np.number):
-                    climatology_ctx.out_of += 1
-                    climatology_ctx.messages.append(
-                        f"Climatology variable {clim_var.name} is not a numeric type"
-                    )
-                # IMPLEMENTATION CONFORMANCE REQUIRED 6/6
-                if hasattr(clim_var, "_FillValue") or hasattr(
-                    clim_var, "missing_value"
-                ):
-                    climatology_ctx.out_of += 1
-                    climatology_ctx.messages.append(
-                        f"Climatology variable {clim_var.name} may not contain"
-                        "attributes _FillValue or missing_value"
-                    )
-
-                # IMPLEMENTATION CONFORMANCE 7.4 REQUIRED 5/6
-                for same_attr in ("units", "standard_name", "calendar"):
-                    if hasattr(clim_var, same_attr):
-                        climatology_ctx.assert_true(
-                            getattr(clim_var, same_attr)
-                            == getattr(clim_coord_var, same_attr, None),
-                            f"Attribute {same_attr} must have the same value in both "
-                            "variables {clim_var.name} and {clim_coord_var.name}",
-                        )
-            ret_val.append(climatology_ctx.to_result())
+                result = Result(
+                    BaseCheck.MEDIUM, False, (self.section_titles["7.4"]), reasoning
+                )
+                ret_val.append(result)
 
             # check that coordinate bounds are in the proper order.
             # make sure last elements are boundary variable specific dimensions
-            # IMPLEMENTATION CONFORMANCE 7.4 REQUIRED 3/6
             if (
                 clim_coord_var.dimensions[:]
                 != ds.variables[clim_coord_var.climatology].dimensions[
@@ -3007,16 +2815,11 @@ class CF1_6Check(CFNCCheck):
                 )
                 ret_val.append(result)
 
-            # IMPLEMENTATION CONFORMANCE 7.4 REQUIRED 3/6 - dim size of 2 for
-            # climatology-specific dimension
-            elif (
-                ds.dimensions[
+            elif (ds.dimensions[
                     ds.variables[clim_coord_var.climatology].dimensions[-1]
-                ].size
-                != 2
-            ):
+                ].size != 2):
                 reasoning.append(
-                    'Climatology dimension "{}" should only contain two elements'.format(
+                    "Climatology dimension \"{}\" should only contain two elements".format(
                         ds.variables[clim_coord_var.climatology].name
                     )
                 )
@@ -3121,18 +2924,11 @@ class CF1_6Check(CFNCCheck):
             if not scale_factor:
                 scale_factor = add_offset
 
-            # IMPLEMENTATION CONFORMANCE 8.1 REQUIRED 1/3
-            # scale_factor and add_offset same type
             if type(add_offset) != type(scale_factor):
                 valid = False
                 reasoning.append(
                     "Attributes add_offset and scale_factor have different data type."
                 )
-            # IMPLEMENTATION CONFORMANCE 8.1 REQUIRED 2/3
-            # scale_factor and add_offset must be floating point or double
-            # if not the same type
-            # FIXME: Check add_offset too.
-
             elif type(scale_factor) != var.dtype.type:
                 # Check both attributes are type float or double
                 if not isinstance(scale_factor, (float, np.floating)):
@@ -3150,11 +2946,7 @@ class CF1_6Check(CFNCCheck):
                         np.int64,
                     ]:
                         valid = False
-                        # IMPLEMENTATION CONFORMANCE REQUIRED 3/3
-                        # IMPLEMENTATION CONFORMANCE REQUIRED 3/3
-                        reasoning.append(
-                            "Variable is not of type byte, short, or int as required for different type add_offset/scale_factor."
-                        )
+                        reasoning.append("Variable is not of type byte, short, or int.")
 
             result = Result(
                 BaseCheck.MEDIUM, valid, self.section_titles["8.1"], reasoning
@@ -3253,7 +3045,6 @@ class CF1_6Check(CFNCCheck):
                         compress_var.name
                     )
                 )
-            # IMPLEMENTATION CONFORMANCE 8.2 REQUIRED 1/3
             # ensure compression variable is a proper index, and thus is an
             # signed or unsigned integer type of some sort
             if (compress_var.dtype is str) or (
@@ -3265,7 +3056,6 @@ class CF1_6Check(CFNCCheck):
                         compress_var.name
                     )
                 )
-            # IMPLEMENTATION CONFORMANCE 8.2 REQUIRED 2/3
             # make sure all the variables referred to are contained by the
             # variables.
             if not compress_set.issubset(ds.dimensions):
@@ -3445,3 +3235,4 @@ class CF1_6Check(CFNCCheck):
                 ret_val.append(result)
 
         return ret_val
+
diff --git a/compliance_checker/cf/cf_1_7.py b/compliance_checker/cf/cf_1_7.py
index c472818..486ff6d 100644
--- a/compliance_checker/cf/cf_1_7.py
+++ b/compliance_checker/cf/cf_1_7.py
@@ -1,18 +1,24 @@
 import logging
 import os
 import sqlite3
+import sys
 
+from collections import OrderedDict, defaultdict
+from functools import wraps
 from warnings import warn
 
 import numpy as np
 import pyproj
 import regex
 
+from cf_units import Unit
 
 from compliance_checker import cfutil
-from compliance_checker.base import BaseCheck, Result, TestCtx
+from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
+from compliance_checker.cf import util
 from compliance_checker.cf.appendix_d import (
     dimless_vertical_coordinates_1_7,
+    no_missing_terms,
 )
 from compliance_checker.cf.appendix_e import cell_methods17
 from compliance_checker.cf.appendix_f import (
@@ -27,7 +33,6 @@ from compliance_checker.cf.cf_1_6 import CF1_6Check
 
 logger = logging.getLogger(__name__)
 
-
 class CF1_7Check(CF1_6Check):
     """Implementation for CF v1.7. Inherits from CF1_6Check as most of the
     checks are the same."""
@@ -54,6 +59,11 @@ class CF1_7Check(CF1_6Check):
                 "attr_loc": {"G"},
                 "cf_section": "2.6.3",
             },
+            "actual_range": {
+                "Type": "N",
+                "attr_loc": {"D", "C"},
+                "cf_section": "2.5.1",
+            },
             "scale_factor": {"Type": "N", "attr_loc": {"D", "C"}, "cf_section": "8.1"},
         }
     )
@@ -65,44 +75,8 @@ class CF1_7Check(CF1_6Check):
         self.grid_mapping_dict = grid_mapping_dict17
         self.grid_mapping_attr_types = grid_mapping_attr_types17
 
-    def check_external_variables(self, ds):
-        """
-        The global external_variables attribute is a blank-separated list of the
-        names of variables which are named by attributes in the file but which
-        are not present in the file. These variables are to be found in other
-        files (called "external files") but CF does not provide conventions for
-        identifying the files concerned. The only attribute for which CF
-        standardises the use of external variables is cell_measures.
-
-        :param netCDF4.Dataset ds: An open netCDF dataset
-        :rtype: compliance_checker.base.Result
-        """
-        external_vars_ctx = TestCtx(BaseCheck.MEDIUM, self.section_titles["2.6.3"])
-        # IMPLEMENTATION CONFORMANCE 2.6.3 REQUIRED 2/2
-        try:
-            external_var_names = set(ds.external_variables.strip().split())
-
-            bad_external_var_names = external_var_names.intersection(ds.variables)
-
-            if bad_external_var_names:
-                external_vars_ctx.out_of += 1
-                bad_msg = (
-                    "Global attribute external_variables should not "
-                    "have any variable names which are present in the dataset. "
-                    "Currently, the following names appear in both external_variables "
-                    f"and the dataset's variables: {bad_external_var_names}"
-                )
-                external_vars_ctx.messages.append(bad_msg)
-
-        # string/global attributes are handled in Appendix A checks
-        except (AttributeError, ValueError):
-            pass
-
-        return external_vars_ctx.to_result()
-
     def check_actual_range(self, ds):
-        """
-        Check the actual_range attribute of variables. As stated in
+        """Check the actual_range attribute of variables. As stated in
         section 2.5.1 of version 1.7, this convention defines a two-element
         vector attribute designed to describe the actual minimum and actual
         maximum values of variables containing numeric data. Conditions:
@@ -726,7 +700,7 @@ class CF1_7Check(CF1_6Check):
             return (True, msg)
 
     def check_grid_mapping(self, ds):
-        super(CF1_7Check, self).check_grid_mapping.__doc__
+        __doc__ = super(CF1_7Check, self).check_grid_mapping.__doc__
         prev_return = super(CF1_7Check, self).check_grid_mapping(ds)
         grid_mapping_variables = cfutil.get_grid_mapping_variables(ds)
         for var_name in sorted(grid_mapping_variables):
@@ -781,7 +755,8 @@ class CF1_7Check(CF1_6Check):
                 )
             elif len_vdatum_name_attrs == 1:
                 # should be one or zero attrs
-                proj_db_path = os.path.join(pyproj.datadir.get_data_dir(), "proj.db")
+                proj_db_path = os.path.join(pyproj.datadir.get_data_dir(),
+                                            "proj.db")
                 try:
                     with sqlite3.connect(proj_db_path) as conn:
                         v_datum_attr = next(iter(vert_datum_attrs))
@@ -808,18 +783,6 @@ class CF1_7Check(CF1_6Check):
 
         return prev_return
 
-    def check_standard_name_deprecated_modifiers(self, ds):
-        """
-        Not a standard check in that it won't raise pass/fail values,
-        but instead warns upon finding deprecated CF standard name modifiers.
-        :param netCDF4.Dataset ds: netCDF dataset
-        """
-        deprecated_var_names = cfutil._find_standard_name_modifier_variables(ds, True)
-        if deprecated_var_names:
-            warn(
-                f"Deprecated standard_name modifiers found on variables {deprecated_var_names}"
-            )
-
     def _process_v_datum_str(self, v_datum_str, conn):
         vdatum_query = """SELECT 1 FROM alias_name WHERE
                             table_name = 'vertical_datum' AND
diff --git a/compliance_checker/cf/cf_1_8.py b/compliance_checker/cf/cf_1_8.py
index f2a1554..9ba3637 100644
--- a/compliance_checker/cf/cf_1_8.py
+++ b/compliance_checker/cf/cf_1_8.py
@@ -1,7 +1,8 @@
-from compliance_checker.base import BaseCheck, TestCtx
+from compliance_checker.base import BaseCheck, TestCtx, Result
 from compliance_checker import MemoizedDataset
 from compliance_checker.cf.cf_1_7 import CF1_7Check
 from netCDF4 import Dataset
+from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
 import requests
 from lxml import etree
 from shapely.geometry import Polygon
@@ -10,6 +11,9 @@ import re
 from compliance_checker.cf.util import reference_attr_variables, string_from_var_type
 import itertools
 import warnings
+from shapely.geometry import (MultiPoint, LineString, MultiLineString, Polygon,
+                              MultiPolygon)
+from compliance_checker.cf.util import reference_attr_variables
 
 """
 What's new in CF-1.8
@@ -36,13 +40,11 @@ class CF1_8Check(CF1_7Check):
 
     def __init__(self, options=None):
         super(CF1_8Check, self).__init__(options)
-        self.section_titles.update(
-            {
-                "2.7": "§2.7 Groups",
-                "6.1.2": "§6.1.2 Taxon Names and Identifiers",
-                "7.5": "§7.5 Geometries",
-            }
-        )
+        self.section_titles.update({"2.7":
+                                    "§2.7 Groups",
+                                    "6.1.2":
+                                    "§6.1.2 Taxon Names and Identifiers",
+                                    "7.5": "§7.5 Geometries"})
 
     def check_groups(self, ds: MemoizedDataset):
         """
@@ -118,8 +120,7 @@ class CF1_8Check(CF1_7Check):
         :returns list: List of error messages
         """
         vars_with_geometry = ds.get_variables_by_attributes(
-            geometry=lambda g: g is not None
-        )
+                                geometry=lambda g: g is not None)
         results = []
         unique_geometry_var_names = {var.geometry for var in vars_with_geometry}
         if unique_geometry_var_names:
@@ -127,30 +128,27 @@ class CF1_8Check(CF1_7Check):
             geom_valid.out_of += 1
         for geometry_var_name in unique_geometry_var_names:
             if geometry_var_name not in ds.variables:
-                geom_valid.messages.append(
-                    "Cannot find geometry variable " f"named {geometry_var_name}"
-                )
+                geom_valid.messages.append("Cannot find geometry variable "
+                                           f"named {geometry_var_name}")
                 results.append(geom_valid.to_result())
                 continue
             else:
                 geometry_var = ds.variables[geometry_var_name]
 
             geometry_type = getattr(geometry_var, "geometry_type")
+            valid_geometry_types = {"point", "line", "polygon"}
             try:
                 node_coord_var_names = geometry_var.node_coordinates
-            except AttributeError:
-                geom_valid.messages.append(
-                    "Could not find required attribute "
-                    '"node_coordinates" in geometry '
-                    f'variable "{geometry_var_name}"'
-                )
+            except AttributeError as e:
+                geom_valid.messsages.append('Could not find required attribute '
+                                            '"node_coordinates" in geometry '
+                                            f'variable "{geometry_var_name}"')
                 results.append(geom_valid.to_result())
             if not isinstance(node_coord_var_names, str):
                 geom_valid.messages.append(
-                    'Attribute "node_coordinates" in geometry '
-                    f'variable "{geometry_var_name}" must be '
-                    "a string"
-                )
+                                  'Attribute "node_coordinates" in geometry '
+                                  f'variable "{geometry_var_name}" must be '
+                                  'a string')
                 results.append(geom_valid.to_result())
                 continue
             split_coord_names = node_coord_var_names.strip().split(" ")
@@ -163,41 +161,38 @@ class CF1_8Check(CF1_7Check):
             # If any variables weren't found, we can't continue
             if not_found_node_vars:
                 geom_valid.messages.append(
-                    "The following referenced node coordinate"
-                    "variables for geometry variable"
-                    f'"{geometry_var_name}" were not found: '
-                    f"{not_found_node_vars}"
-                )
+                                  "The following referenced node coordinate"
+                                  "variables for geometry variable"
+                                  f'"{geometry_var_name}" were not found: '
+                                  f'{not_found_node_vars}')
                 results.append(geom_valid.to_result())
                 continue
+                return error_msgs
 
-            node_count = reference_attr_variables(
-                ds, getattr(geometry_var, "node_count", None)
-            )
+            node_count = reference_attr_variables(ds,
+                               getattr(geometry_var, "node_count", None))
             # multipart lines and polygons only
-            part_node_count = reference_attr_variables(
-                ds, getattr(geometry_var, "part_node_count", None)
-            )
+            part_node_count = reference_attr_variables(ds,
+                                getattr(geometry_var, "part_node_count", None))
             # polygons with interior geometry only
-            interior_ring = reference_attr_variables(
-                ds, getattr(geometry_var, "interior_ring", None)
-            )
+            interior_ring = reference_attr_variables(ds,
+                               getattr(geometry_var, "interior_ring", None))
 
             if geometry_type == "point":
                 geometry = PointGeometry(node_coord_vars, node_count)
             elif geometry_type == "line":
-                geometry = LineGeometry(node_coord_vars, node_count, part_node_count)
+                geometry = LineGeometry(node_coord_vars, node_count,
+                                        part_node_count)
             elif geometry_type == "polygon":
-                geometry = PolygonGeometry(
-                    node_coord_vars, node_count, part_node_count, interior_ring
-                )
+                geometry = PolygonGeometry(node_coord_vars, node_count,
+                                                    part_node_count,
+                                                    interior_ring)
             else:
                 geom_valid.messages.append(
-                    f'For geometry variable "{geometry_var_name}'
-                    'the attribute "geometry_type" must exist'
-                    "and have one of the following values:"
-                    '"point", "line", "polygon"'
-                )
+                                  f'For geometry variable "{geometry_var_name}'
+                                  'the attribute "geometry_type" must exist'
+                                  'and have one of the following values:'
+                                  '"point", "line", "polygon"')
                 results.append(geom_valid.to_result())
                 continue
             # check geometry
@@ -240,7 +235,12 @@ class CF1_8Check(CF1_7Check):
         given for those taxa that do not have an identifier.
         """
         ret_val = []
-
+        # taxa identification variables
+        taxa_name_variables = ds.get_variables_by_attributes(
+                                                          standard_name="biological_taxon_name")
+        taxa_lsid_variables = ds.get_variables_by_attributes(
+                                                          standard_name="biological_taxon_identifier")
+        
         def match_taxa_standard_names(standard_name_string):
             """
             Match variables which are standard_names related to taxa, but
@@ -483,10 +483,10 @@ class GeometryStorage(object):
 
     def _split_mulitpart_geometry(self):
         arr_extents_filt = self.part_node_count[self.part_node_count > 0]
-        splits = np.split(np.vstack(self.coord_vars).T, arr_extents_filt.cumsum()[:-1])
+        splits = np.split(np.vstack(self.coord_vars).T,
+                          arr_extents_filt.cumsum()[:-1])
         return splits
 
-
 class PointGeometry(GeometryStorage):
     """Class for validating Point/MultiPoint geometries"""
 
@@ -494,26 +494,24 @@ class PointGeometry(GeometryStorage):
         super().check_geometry()
         # non-multipoint should have exactly one feature
         if self.node_count is None:
-            pass
+            expected_node_count = 1
         else:
-            self.node_count
+            expected_node_count = self.node_count
 
         if all(len(cv.dimensions) != 0 for cv in self.coord_vars):
-            same_dim_group = itertools.groupby(self.coord_vars, lambda x: x.dimensions)
-            same_dim = next(same_dim_group, True) and not next(same_dim_group, False)
+            same_dim_group = itertools.groupby(self.coord_vars,
+                                            lambda x: x.dimensions)
+            same_dim = (next(same_dim_group, True) and
+                             not next(same_dim_group, False))
             if not same_dim:
-                self.errors.append(
-                    "For a point geometry, coordinate "
-                    "variables must be the same length as "
-                    "node_count defined, or must be "
-                    "length 1 if node_count is not set"
-                )
+                self.errors.append("For a point geometry, coordinate "
+                                   "variables must be the same length as "
+                                   "node_count defined, or must be "
+                                   "length 1 if node_count is not set")
         return self.errors
 
-
 class LineGeometry(GeometryStorage):
     """Class for validating Line/MultiLine geometries"""
-
     def __init__(self, coord_vars, node_count, part_node_count):
         super().__init__(coord_vars, node_count)
         self.part_node_count = part_node_count
@@ -522,46 +520,40 @@ class LineGeometry(GeometryStorage):
 
     def check_geometry(self):
         geom_errors = []
-        same_dim_group = itertools.groupby(self.coord_vars, lambda x: x.dimensions)
-        same_dim = next(same_dim_group, True) and not next(same_dim_group, False)
+        same_dim_group = itertools.groupby(self.coord_vars,
+                                           lambda x: x.dimensions)
+        same_dim = (next(same_dim_group, True) and
+                         not next(same_dim_group, False))
         if not same_dim:
-            raise IndexError(
-                "Coordinate variables must be the same length. "
-                "If node_count is specified, this value must "
-                "also sum to the length of the coordinate "
-                "variables."
-            )
+            raise IndexError("Coordinate variables must be the same length. "
+                            "If node_count is specified, this value must "
+                            "also sum to the length of the coordinate "
+                            "variables.")
         # if a multipart
         if self.node_count is not None:
             same_length = len(self.coord_vars[0]) == self.node_count[:].sum()
             if not same_length:
-                geom_errors.append(
-                    "Coordinate variables must be the same "
-                    "length. If node_count is specified, this "
-                    "value must also sum to the length of the "
-                    "coordinate variables."
-                )
+                geom_errors.append("Coordinate variables must be the same "
+                                   "length. If node_count is specified, this "
+                                   "value must also sum to the length of the "
+                                    "coordinate variables.")
         if self.part_node_count is not None:
             if not np.issubdtype(self.part_node_count.dtype, np.integer):
-                geom_errors.append(
-                    "when part_node_count is specified, it must "
-                    "be an array of integers"
-                )
+                geom_errors.append("when part_node_count is specified, it must "
+                                   "be an array of integers")
             same_node_count = len(self.coord_vars[0]) == self.node_count[:].sum()
             if not same_node_count:
-                geom_errors.append(
-                    "The sum of part_node_count must be equal "
-                    "to the value of node_count"
-                )
+                geom_errors.append("The sum of part_node_count must be equal "
+                                   "to the value of node_count")
         return geom_errors
 
 
 class PolygonGeometry(LineGeometry):
     """Class for validating Line/MultiLine geometries"""
-
     # TODO/clarify: Should polygons be simple, i.e. non-self intersecting?
     # Presumably
-    def __init__(self, coord_vars, node_count, part_node_count, interior_ring):
+    def __init__(self, coord_vars, node_count, part_node_count,
+                    interior_ring):
         super().__init__(coord_vars, node_count, part_node_count)
         self.part_node_count = part_node_count
         self.interior_ring = interior_ring
@@ -586,9 +578,7 @@ class PolygonGeometry(LineGeometry):
         try:
             polygon = Polygon(transposed_coords.tolist())
         except ValueError:
-            raise ValueError(
-                "Polygon contains too few points to perform orientation test"
-            )
+            raise ValueError("Polygon contains too few points to perform orientation test")
 
         ccw = polygon.exterior.is_ccw
         return not ccw if interior else ccw
@@ -600,14 +590,17 @@ class PolygonGeometry(LineGeometry):
         if messages:
             return messages
         if self.part_node_count is not None:
-            extents = np.concatenate([np.array([0]), self.part_node_count[:].cumsum()])
+            extents = np.concatenate([np.array([0]),
+                                     self.part_node_count[:].cumsum()])
             if self.interior_ring is not None:
                 ring_orientation = self.interior_ring[:].astype(bool)
             else:
                 ring_orientation = np.zeros(len(self.part_count), dtype=bool)
+            current_node_count = self.node_count[:].copy()
             node_indexer_len = len(self.part_node_count)
         else:
-            extents = np.concatenate([np.array([0]), self.node_count[:].cumsum()])
+            extents = np.concatenate([np.array([0]),
+                                     self.node_count[:].cumsum()])
             node_indexer_len = len(self.node_count)
             ring_orientation = np.zeros(node_indexer_len, dtype=bool)
         # TODO: is it necessary to check whether part_node_count "consumes"
@@ -615,22 +608,19 @@ class PolygonGeometry(LineGeometry):
         #       a node part of 9, follow by next 3 will consume a node part of
         #       3 after consuming
         for i in range(node_indexer_len):
-            extent_slice = slice(extents[i], extents[i + 1])
-            poly_sliced = np.vstack([cv[extent_slice] for cv in self.coord_vars]).T
-            pass_orientation = self.check_polygon_orientation(
-                poly_sliced, ring_orientation[i]
-            )
+            extent_slice = slice(extents[i], extents[i+1])
+            poly_sliced = np.vstack([cv[extent_slice] for cv in
+                                     self.coord_vars]).T
+            pass_orientation = (self.check_polygon_orientation(
+                                            poly_sliced,
+                                            ring_orientation[i]))
             if not pass_orientation:
-                orient_fix = (
-                    ("exterior", "counterclockwise")
-                    if not ring_orientation[i]
-                    else ("interior", "clockwise")
-                )
-                message = (
-                    f"An {orient_fix[0]} polygon referred to by "
-                    f"coordinates ({poly_sliced}) must have coordinates "
-                    f"in {orient_fix[1]} order"
-                )
+                orient_fix = (("exterior", "counterclockwise")
+                                 if not ring_orientation[i] else
+                               ("interior", "clockwise"))
+                message = (f"An {orient_fix[0]} polygon referred to by "
+                           f"coordinates ({poly_sliced}) must have coordinates "
+                           f"in {orient_fix[1]} order")
                 messages.append(message)
         return messages
 
diff --git a/compliance_checker/cf/cf_base.py b/compliance_checker/cf/cf_base.py
index 895d208..ab6ebf2 100644
--- a/compliance_checker/cf/cf_base.py
+++ b/compliance_checker/cf/cf_base.py
@@ -10,17 +10,29 @@ from warnings import warn
 import numpy as np
 import regex
 
+from cf_units import Unit
 
 from compliance_checker import cfutil
 from compliance_checker.base import BaseCheck, BaseNCCheck, Result, TestCtx
 from compliance_checker.cf import util
 from compliance_checker.cf.appendix_d import (
+    dimless_vertical_coordinates_1_6,
+    dimless_vertical_coordinates_1_7,
     no_missing_terms,
 )
+from compliance_checker.cf.appendix_e import cell_methods16, cell_methods17
+from compliance_checker.cf.appendix_f import (
+    ellipsoid_names17,
+    grid_mapping_attr_types16,
+    grid_mapping_attr_types17,
+    grid_mapping_dict16,
+    grid_mapping_dict17,
+    horizontal_datum_names17,
+    prime_meridian_names17,
+)
 
 logger = logging.getLogger(__name__)
 
-
 class CFBaseCheck(BaseCheck):
     """
     CF Convention Checker Base
@@ -45,13 +57,11 @@ class CFBaseCheck(BaseCheck):
         self._std_names = util.StandardNameTable()
 
         self.section_titles = {  # dict of section headers shared by grouped checks
-            "2.1": "§2.1 Filename",
             "2.2": "§2.2 Data Types",
             "2.3": "§2.3 Naming Conventions",
             "2.4": "§2.4 Dimensions",
             "2.5": "§2.5 Variables",
             "2.6": "§2.6 Attributes",
-            "2.6.3": "§2.6.3 External Variables",
             "3.1": "§3.1 Units",
             "3.2": "§3.2 Long Name",
             "3.3": "§3.3 Standard Name",
@@ -188,7 +198,7 @@ class CFBaseCheck(BaseCheck):
             defines_grid_mapping.assert_true(
                 (isinstance(grid_mapping, str) and grid_mapping),
                 "{}'s grid_mapping attribute must be a "
-                "space-separated non-empty string".format(variable.name),
+                + "space-separated non-empty string".format(variable.name),
             )
             if isinstance(grid_mapping, str):
                 # TODO (badams): refactor functionality to split functionality
@@ -1215,6 +1225,8 @@ class CFNCCheck(BaseNCCheck, CFBaseCheck):
     attributes from BaseNCCheck (like supported_ds) will not be passed to
     CFNCCheck."""
 
+    pass
+
 
 appendix_a_base = {
     "Conventions": {"Type": "S", "attr_loc": {"G"}, "cf_section": None},
@@ -1242,6 +1254,7 @@ appendix_a_base = {
     "formula_terms": {"Type": "S", "attr_loc": {"C"}, "cf_section": "4.3.2"},
     "grid_mapping": {"Type": "S", "attr_loc": {"D"}, "cf_section": "5.6"},
     "history": {"Type": "S", "attr_loc": {"G"}, "cf_section": None},
+    #'instance_dimension': {'Type': 'N', 'attr_loc': {'D'}, 'cf_section': '9.3'},
     "institution": {"Type": "S", "attr_loc": {"G", "D"}, "cf_section": "2.6.2"},
     "leap_month": {"Type": "N", "attr_loc": {"C"}, "cf_section": "4.4.1"},
     "leap_year": {"Type": "N", "attr_loc": {"C"}, "cf_section": "4.4.1"},
@@ -1250,6 +1263,7 @@ appendix_a_base = {
     "month_lengths": {"Type": "N", "attr_loc": {"C"}, "cf_section": "4.4.1"},
     "positive": {"Type": "S", "attr_loc": {"C"}, "cf_section": None},
     "references": {"Type": "S", "attr_loc": {"G", "D"}, "cf_section": "2.6.2"},
+    #'sample_dimension': {'Type': 'N', 'attr_loc': {'D'}, 'cf_section': '9.3'},
     "scale_factor": {"Type": "N", "attr_loc": {"D"}, "cf_section": "8.1"},
     "source": {"Type": "S", "attr_loc": {"G", "D"}, "cf_section": "2.6.2"},
     "standard_error_multiplier": {"Type": "N", "attr_loc": {"D"}, "cf_section": None},
@@ -1260,3 +1274,8 @@ appendix_a_base = {
     "valid_min": {"Type": "N", "attr_loc": {"D", "C"}, "cf_section": None},
     "valid_range": {"Type": "N", "attr_loc": {"D", "C"}, "cf_section": None},
 }
+
+class CFNCCheck(BaseNCCheck, CFBaseCheck):
+    @classmethod
+    def beliefs(cls):  # @TODO
+        return {}
diff --git a/compliance_checker/cf/util.py b/compliance_checker/cf/util.py
index e36966f..4e97a38 100644
--- a/compliance_checker/cf/util.py
+++ b/compliance_checker/cf/util.py
@@ -235,7 +235,6 @@ def get_safe(dict_instance, keypath, default=None):
     except Exception:
         return default
 
-
 class VariableReferenceError(Exception):
     """A variable to assign bad variable references to"""
 
@@ -501,12 +500,6 @@ def units_temporal(units):
         u = Unit(units)
     except ValueError:
         return False
-    # IMPLEMENTATION CONFORMANCE REQUIRED 4.4 1/3
-    # time units of a time_coordinate varialbe must contain a reference
-    # time/date
-    # IMPLEMENTATION CONFORMANCE REQUIRED 4.4 3/3
-    # check that reference time seconds is not greater than or
-    # equal to 60
     return u.is_time_reference()
 
 
@@ -583,36 +576,6 @@ def is_vertical_coordinate(var_name, var):
     return satisfied
 
 
-def compare_unit_types(specified, reference):
-    """
-    Compares two unit strings via UDUnits
-
-    :param str specified: The specified unit
-    :param str reference: The reference unit which to compare against
-
-    """
-    msgs = []
-    err_flag = False
-    try:
-        specified_unit = Unit(specified)
-    except ValueError:
-        msgs.append(f"Specified conversion unit f{specified} may not be valid UDUnits")
-        err_flag = True
-
-    try:
-        reference_unit = Unit(reference)
-    except ValueError:
-        msgs.append(f"Specified conversion unit f{reference} may not be valid UDUnits")
-        err_flag = True
-
-    if err_flag:
-        return msgs
-
-    unit_convertible = specified_unit.is_convertible(reference_unit)
-    fail_msg = [f'Units "{specified}" are not convertible to "{reference}"']
-    return msgs if unit_convertible else fail_msg
-
-
 def string_from_var_type(variable):
     if isinstance(variable, str):
         return variable[:]
diff --git a/compliance_checker/cfutil.py b/compliance_checker/cfutil.py
index da5f839..414e473 100644
--- a/compliance_checker/cfutil.py
+++ b/compliance_checker/cfutil.py
@@ -100,7 +100,7 @@ def attr_membership(attr_val, value_set, attr_type=str, modifier_fn=lambda x: x)
 
 
 @lru_cache(128)
-def is_dimensionless_standard_name(standard_name_table, standard_name):
+def is_dimensionless_standard_name(xml_tree, standard_name):
     """
     Returns True if the units for the associated standard name are
     dimensionless.  Dimensionless standard names include those that have no
@@ -110,12 +110,15 @@ def is_dimensionless_standard_name(standard_name_table, standard_name):
     # standard_name must be string, so if it is not, it is *wrong* by default
     if not isinstance(standard_name, str):
         return False
-    found_standard_name = standard_name_table.find(
-        ".//entry[@id='{}']".format(standard_name)
-    )
+    found_standard_name = xml_tree.find(".//entry[@id='{}']".format(standard_name))
     if found_standard_name is not None:
-        canonical_units = Unit(found_standard_name.find("canonical_units").text)
-        return canonical_units.is_dimensionless()
+        canonical_units = found_standard_name.find("canonical_units")
+        # so far, standard name XML table includes
+        # 1 and 1e-3 for constant units, but expanding to valid udunits
+        # prefixes to be on the safe side
+        # taken from CF Table 3.1 of valid UDUnits prefixes
+        dimless_units = r"1(?:e-?(?:1|2|3|6|9|12|15|18|21|24))?$"
+        return canonical_units is None or re.match(dimless_units, canonical_units.text)
     # if the standard name is not found, assume we need units for the time being
     else:
         return False
@@ -376,12 +379,6 @@ def get_cell_boundary_variables(ds):
     return boundary_variables
 
 
-@lru_cache(128)
-def get_bounds_variables(ds):
-    contains_bounds = ds.get_variables_by_attributes(bounds=lambda s: s in ds.variables)
-    return {ds.variables[parent_var.bounds] for parent_var in contains_bounds}
-
-
 @lru_cache(128)
 def get_geophysical_variables(ds):
     """
@@ -390,9 +387,10 @@ def get_geophysical_variables(ds):
 
     :param netCDF4.Dataset nc: An open netCDF dataset
     """
+
     parameters = []
     for variable in ds.variables:
-        if is_geophysical(ds, variable) and variable not in get_bounds_variables(ds):
+        if is_geophysical(ds, variable):
             parameters.append(variable)
     return parameters
 
@@ -733,27 +731,6 @@ def get_climatology_variable(ds):
     return None
 
 
-@lru_cache(128)
-def _find_standard_name_modifier_variables(ds, return_deprecated=False):
-    def match_modifier_variables(standard_name_str):
-        if standard_name_str is None:
-            return False
-        if not return_deprecated:
-            matches = re.search(r"^\w+ +\w+", standard_name_str)
-        else:
-            matches = re.search(
-                r"^\w+ +(?:status_flag|number_of_observations)$", standard_name_str
-            )
-        return bool(matches)
-
-    return [
-        var.name
-        for var in ds.get_variables_by_attributes(
-            standard_name=match_modifier_variables
-        )
-    ]
-
-
 def get_flag_variables(ds):
     """
     Returns a list of variables that are defined as flag variables
diff --git a/compliance_checker/ioos.py b/compliance_checker/ioos.py
index 6c9a165..6a21522 100644
--- a/compliance_checker/ioos.py
+++ b/compliance_checker/ioos.py
@@ -20,6 +20,7 @@ from compliance_checker.base import (
     BaseSOSGCCheck,
     Result,
     TestCtx,
+    attr_check,
     check_has,
 )
 from compliance_checker.cf import util as cf_util  # not to be confused with cfutil.py
@@ -553,7 +554,7 @@ class IOOS1_2Check(IOOSNCCheck):
             ("infoUrl", base.UrlValidator()),
             "license",
             ("naming_authority", NamingAuthorityValidator()),
-            #'platform', # checked in check_platform_global # noqa
+            #'platform', # checked in check_platform_global
             "platform_name",
             "publisher_country",
             ("publisher_email", base.EmailValidator()),
@@ -579,11 +580,11 @@ class IOOS1_2Check(IOOSNCCheck):
             "creator_postalcode",
             "creator_state",
             # checked in check_creator_and_publisher_type
-            #'creator_type', # noqa
+            #'creator_type',
             "institution",
             "instrument",
             # checked in check_ioos_ingest
-            #'ioos_ingest', # noqa
+            #'ioos_ingest',
             "keywords",
             ("platform_id", IOOS1_2_PlatformIDValidator()),  # alphanumeric only
             "publisher_address",
@@ -593,7 +594,7 @@ class IOOS1_2Check(IOOSNCCheck):
             "publisher_postalcode",
             "publisher_state",
             # checked in check_creator_and_publisher_type
-            #'publisher_type', # noqa
+            #'publisher_type',
             "references",
             "instrument_vocabulary",
         ]
@@ -733,7 +734,7 @@ class IOOS1_2Check(IOOSNCCheck):
                             None if role_val else [role_msg.format(_role)],
                         )
                     )
-            except TypeError:
+            except TypeError as e:
                 role_results.append(
                     Result(
                         BaseCheck.MEDIUM,
@@ -766,7 +767,7 @@ class IOOS1_2Check(IOOSNCCheck):
                             None if vocb_val else [vocb_msg.format(_vocb)],
                         )
                     )
-            except TypeError:
+            except TypeError as e:
                 vocb_results.append(
                     Result(
                         BaseCheck.MEDIUM,
@@ -934,7 +935,7 @@ class IOOS1_2Check(IOOSNCCheck):
                 [
                     (
                         f"Invalid featureType '{feature_type_attr}'; please see the "
-                        "IOOS 1.2 Profile and CF-1.7 Conformance documents for valid featureType"
+                         "IOOS 1.2 Profile and CF-1.7 Conformance documents for valid featureType"
                     )
                 ],
             )
@@ -958,8 +959,9 @@ class IOOS1_2Check(IOOSNCCheck):
 
         elif feature_type == "point":
             return Result(
-                BaseCheck.MEDIUM, True, "CF DSG: featureType=trajectoryProfile"
-            )
+                BaseCheck.MEDIUM,
+                True,
+                "CF DSG: featureType=trajectoryProfile")
 
         else:
             return Result(
@@ -969,8 +971,8 @@ class IOOS1_2Check(IOOSNCCheck):
                 [
                     (
                         f"Invalid featureType '{feature_type_attr}'; "
-                        "please see the IOOS 1.2 Profile and CF-1.7 "
-                        "Conformance documents for valid featureType"
+                          "please see the IOOS 1.2 Profile and CF-1.7 "
+                          "Conformance documents for valid featureType"
                     )
                 ],
             )
@@ -1441,6 +1443,8 @@ class IOOS1_2Check(IOOSNCCheck):
         bool
         """
 
+        val = False
+
         # should have an ancillary variable with standard_name aggregate_quality_flag
         avar_val = False
         anc_vars = str(getattr(var, "ancillary_variables", "")).split(" ")
@@ -1501,7 +1505,7 @@ class IOOS1_2Check(IOOSNCCheck):
         """
 
         # is dataset properly flagged for ingest?
-        getattr(ds, "gts_ingest", None)
+        glb_gts_attr = getattr(ds, "gts_ingest", None)
 
         # check variables
         all_passed_ingest_reqs = True  # default
diff --git a/compliance_checker/runner.py b/compliance_checker/runner.py
index 9755318..16f2cb1 100644
--- a/compliance_checker/runner.py
+++ b/compliance_checker/runner.py
@@ -77,7 +77,8 @@ class ComplianceChecker(object):
         for loc in locs:  # loop through each dataset and run specified checks
             ds = cs.load_dataset(loc)
 
-            score_groups = cs.run_all(ds, checker_names, include_checks, skip_checks)
+            score_groups = cs.run_all(ds, checker_names, include_checks,
+                                      skip_checks)
             for group in score_groups.values():
                 all_groups.append(group[0])
             # TODO: consider wrapping in a proper context manager instead
@@ -130,7 +131,8 @@ class ComplianceChecker(object):
                     output_filename = "{}.json".format(
                         os.path.splitext(output_filename)[0]
                     )
-                cls.json_output(cs, score_dict, output_filename, ds_loc, limit, out_fmt)
+                cls.json_output(cs, score_dict, output_filename, ds_loc, limit,
+                                out_fmt)
 
             else:
                 raise TypeError("Invalid format %s" % out_fmt)
diff --git a/compliance_checker/suite.py b/compliance_checker/suite.py
index 35cf82b..ad02436 100644
--- a/compliance_checker/suite.py
+++ b/compliance_checker/suite.py
@@ -29,7 +29,8 @@ from pkg_resources import working_set
 
 from compliance_checker import MemoizedDataset, __version__, tempnc
 from compliance_checker.base import BaseCheck, GenericFile, Result, fix_return_value
-from compliance_checker.protocols import cdl, netcdf, opendap
+from compliance_checker.cf.cf_base import CFBaseCheck
+from compliance_checker.protocols import cdl, erddap, netcdf, opendap
 
 
 # Ensure output is encoded as Unicode when checker output is redirected or piped
@@ -91,7 +92,8 @@ class CheckSuite(object):
         :type verbose: int
         """
         for checker in sorted(self.checkers.keys()):
-            version = getattr(self.checkers[checker], "_cc_checker_version", "???")
+            version = getattr(self.checkers[checker], "_cc_checker_version",
+                              "???")
             if verbose > 0:
                 print(" - {} (v{})".format(checker, version))
             elif ":" in checker and not checker.endswith(
@@ -107,7 +109,8 @@ class CheckSuite(object):
         :type checker_obj: subclass of compliance_checker.base.BaseChecker
         """
 
-        check_functions = self._get_checks(checker_obj, defaultdict(lambda: None))
+        check_functions = self._get_checks(checker_obj,
+                                           defaultdict(lambda: None))
         for c, _ in check_functions:
             print("- {}".format(c.__name__))
             if c.__doc__ is not None:
@@ -181,7 +184,8 @@ class CheckSuite(object):
                 print("Could not load", c, ":", e, file=sys.stderr)
         # find the latest version of versioned checkers and set that as the
         # default checker for compliance checker if no version is specified
-        ver_checkers = sorted([c.split(":", 1) for c in cls.checkers if ":" in c])
+        ver_checkers = sorted([c.split(":", 1) for c in cls.checkers if ":"
+                               in c])
         for spec, versions in itertools.groupby(ver_checkers, itemgetter(0)):
             version_nums = [v[-1] for v in versions]
             try:
@@ -215,14 +219,12 @@ class CheckSuite(object):
         if include_checks:
             for fn_name, fn_obj in meths:
                 if fn_name in include_checks:
-                    returned_checks.append((fn_obj, skip_checks[fn_name]))
+                   returned_checks.append((fn_obj, skip_checks[fn_name]))
         else:
             for fn_name, fn_obj in meths:
-                if (
-                    fn_name.startswith("check_")
-                    and skip_checks[fn_name] != BaseCheck.HIGH
-                ):
-                    returned_checks.append((fn_obj, skip_checks[fn_name]))
+                if (fn_name.startswith("check_") and
+                    skip_checks[fn_name] != BaseCheck.HIGH):
+                   returned_checks.append((fn_obj, skip_checks[fn_name]))
 
         return returned_checks
 
@@ -330,7 +332,8 @@ class CheckSuite(object):
 
         check_dict = defaultdict(lambda: None)
         # A is for "all", "M" is for medium, "L" is for low
-        check_lookup = {"A": BaseCheck.HIGH, "M": BaseCheck.MEDIUM, "L": BaseCheck.LOW}
+        check_lookup = {"A": BaseCheck.HIGH, "M": BaseCheck.MEDIUM,
+                        "L": BaseCheck.LOW}
 
         for skip_check_spec in skip_checks:
             split_check_spec = skip_check_spec.split(":")
@@ -369,7 +372,8 @@ class CheckSuite(object):
         return check_dict
 
     def run(self, ds, skip_checks, *checker_names):
-        warnings.warn("suite.run is deprecated, use suite.run_all in calls " "instead")
+        warnings.warn("suite.run is deprecated, use suite.run_all in calls "
+                      "instead")
         return self.run_all(ds, checker_names, skip_checks=skip_checks)
 
     def run_all(self, ds, checker_names, include_checks=None, skip_checks=None):
@@ -682,7 +686,8 @@ class CheckSuite(object):
         # create dict of the groups -> {level: [reasons]}
         result = {
             key: [v for v in valuesiter if v.value[0] != v.value[1]]
-            for key, valuesiter in itertools.groupby(groups_sorted, key=weight_sort)
+            for key, valuesiter in itertools.groupby(groups_sorted,
+                                                     key=weight_sort)
         }
         priorities = self.checkers[check]._cc_display_headers
 
@@ -727,7 +732,8 @@ class CheckSuite(object):
                     print("{:^{width}}".format(level_name, width=width))
                     print("-" * width)
 
-                data_issues = [process_table(res, check) for res in result[level]]
+                data_issues = [process_table(res, check) for res in
+                               result[level]]
 
                 has_printed = False
                 for issue, reasons in data_issues:
@@ -737,7 +743,8 @@ class CheckSuite(object):
                         print("")
                     # join alphabetized reasons together
                     reason_str = "\n".join(
-                        "* {}".format(r) for r in sorted(reasons, key=lambda x: x[0])
+                        "* {}".format(r) for r in sorted(reasons,
+                                                         key=lambda x: x[0])
                     )
                     proc_str = "{}\n{}".format(issue, reason_str)
                     print(proc_str)
diff --git a/compliance_checker/util.py b/compliance_checker/util.py
index a9bb16e..7127965 100644
--- a/compliance_checker/util.py
+++ b/compliance_checker/util.py
@@ -43,6 +43,5 @@ def kvp_convert(input_coll):
         return input_coll
     else:
         return OrderedDict(
-            (thing, None) if not isinstance(thing, tuple) else
-            (thing[0], thing[1]) for thing in input_coll
+            (l, None) if not isinstance(l, tuple) else (l[0], l[1]) for l in input_coll
         )
diff --git a/setup.py b/setup.py
index e58299f..c3ebbce 100644
--- a/setup.py
+++ b/setup.py
@@ -20,6 +20,7 @@ def pip_requirements(fname="requirements.txt"):
     return reqs
 
 
+
 setup(
     name="compliance-checker",
     description="Checks Datasets and SOS endpoints for standards compliance",
@@ -63,7 +64,7 @@ setup(
             "ioos_sos = compliance_checker.ioos:IOOSBaseSOSCheck",
             "ioos-0.1 = compliance_checker.ioos:IOOS0_1Check",
             "ioos-1.1 = compliance_checker.ioos:IOOS1_1Check",
-            "ioos-1.2 = compliance_checker.ioos:IOOS1_2Check",
+            "ioos-1.2 = compliance_checker.ioos:IOOS1_2Check"
         ],
     },
     package_data={


Test patch:
diff --git a/compliance_checker/tests/conftest.py b/compliance_checker/tests/conftest.py
index 94e8e5c..348e76d 100644
--- a/compliance_checker/tests/conftest.py
+++ b/compliance_checker/tests/conftest.py
@@ -9,7 +9,7 @@ import pytest
 from netCDF4 import Dataset
 from pkg_resources import resource_filename
 
-from compliance_checker.cf import util
+from compliance_checker.cf import CF1_6Check, CF1_7Check, util
 from compliance_checker.suite import CheckSuite
 
 
diff --git a/compliance_checker/tests/helpers.py b/compliance_checker/tests/helpers.py
index a49e6b3..19ecd56 100644
--- a/compliance_checker/tests/helpers.py
+++ b/compliance_checker/tests/helpers.py
@@ -8,16 +8,13 @@ class MockNetCDF(Dataset):
     Wrapper object around NetCDF Dataset to write data only to memory.
     """
 
-    def __init__(self, filename=None):
+    def __init__(self):
         # taken from test/tst_diskless.py NetCDF library
         # even though we aren't persisting data to disk, the constructor
         # requires a filename not currently in use by another Dataset object..
-        if filename is None:
-            temp_filename = tempfile.NamedTemporaryFile(suffix=".nc", delete=True).name
-        else:
-            temp_filename = filename
+        tmp_filename = tempfile.NamedTemporaryFile(suffix=".nc", delete=True).name
         super(MockNetCDF, self).__init__(
-            temp_filename, "w", diskless=True, persist=False
+            tmp_filename, "w", diskless=True, persist=False
         )
 
 
@@ -27,20 +24,11 @@ class MockTimeSeries(MockNetCDF):
     variables defined
     """
 
-    def __init__(self, filename=None, default_fill_value=None):
-        super(MockTimeSeries, self).__init__(filename)
+    def __init__(self, default_fill_value=None):
+        super(MockTimeSeries, self).__init__()
         self.createDimension("time", 500)
-        for name, std_name, units in (
-            ("time", "time", "seconds since 1970-01-01"),
-            ("lon", "longitude", "degrees_east"),
-            ("lat", "latitude", "degrees_north"),
-            ("depth", "depth", "m"),
-        ):
-            var = self.createVariable(
-                name, "d", ("time",), fill_value=default_fill_value
-            )
-            var.standard_name = std_name
-            var.units = units
+        for v in ("time", "lon", "lat", "depth"):
+            self.createVariable(v, "d", ("time",), fill_value=default_fill_value)
 
         # give some applicable units
         self.variables["time"].units = "seconds since 2019-04-11T00:00:00"
@@ -74,9 +62,11 @@ class MockVariable(object):
     def __getitem__(self, idx):
         return self._arr[idx]
 
+
     def __setitem__(self, idx, val):
         self._arr[idx] = val
 
+
     def ncattrs(self):
         return [
             att
diff --git a/compliance_checker/tests/test_acdd.py b/compliance_checker/tests/test_acdd.py
index 4e9ea6e..f06ed78 100644
--- a/compliance_checker/tests/test_acdd.py
+++ b/compliance_checker/tests/test_acdd.py
@@ -5,17 +5,17 @@ import numpy as np
 from netCDF4 import Dataset
 
 from compliance_checker.acdd import ACDD1_1Check, ACDD1_3Check
-from compliance_checker.tests import BaseTestCase
-from compliance_checker.tests.helpers import MockTimeSeries
+from compliance_checker.tests import BaseTestCase, pytestBaseTest
+from compliance_checker.tests.helpers import MockTimeSeries, MockVariable
 from compliance_checker.tests.resources import STATIC_FILES
 
 
-def to_singleton_var(item):
+def to_singleton_var(l):
     """
     Get the first value of a list if this implements iterator protocol and is
     not a string
     """
-    return [x[0] if hasattr(x, "__iter__") and not isinstance(x, str) else x for x in item]
+    return [x[0] if hasattr(x, "__iter__") and not isinstance(x, str) else x for x in l]
 
 
 def check_varset_nonintersect(group0, group1):
diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py
index 9fd1818..90786f8 100644
--- a/compliance_checker/tests/test_cf.py
+++ b/compliance_checker/tests/test_cf.py
@@ -33,11 +33,8 @@ from compliance_checker.cf.util import (
 )
 from compliance_checker.suite import CheckSuite
 from compliance_checker.tests import BaseTestCase
-from compliance_checker.tests.helpers import (
-    MockRaggedArrayRepr,
-    MockTimeSeries,
-    MockVariable,
-)
+from compliance_checker.tests.helpers import (MockRaggedArrayRepr,
+                                              MockTimeSeries, MockVariable)
 import requests_mock
 import json
 import re
@@ -93,15 +90,6 @@ class TestCF1_6(BaseTestCase):
         self.addCleanup(nc.close)
         return nc
 
-    def test_extension(self):
-        # TEST CONFORMANCE 2.1
-        ds = MockTimeSeries()
-        result = self.cf.check_filename(ds)
-        assert result.value[0] == result.value[1]
-        ds = MockTimeSeries("filename.notncending")
-        result = self.cf.check_filename(ds)
-        assert result.value[0] != result.value[1]
-
     def test_coord_data_vars(self):
         """Check that coordinate data variables are properly handled"""
         ds = MockTimeSeries()
@@ -134,9 +122,6 @@ class TestCF1_6(BaseTestCase):
         data type, which is also acceptable.
         """
 
-        # TEST CONFORMANCE 2.2 REQUIRED
-        # TODO: Check 1D char array
-
         # check default netCDF data types
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         result = self.cf.check_data_types(dataset)
@@ -260,7 +245,6 @@ class TestCF1_6(BaseTestCase):
         Variable, dimension and attr names should begin with a letter and be composed of letters, digits, and underscores.
         """
 
-        # TEST REQD CONFORMANCE 2.3
         # compliant dataset
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         results = self.cf.check_naming_conventions(dataset)
@@ -274,7 +258,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 3
         assert scored < out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
-        assert all(r.name == "§2.3 Naming Conventions" for r in results)
+        assert all(r.name == u"§2.3 Naming Conventions" for r in results)
 
         # another non-compliant dataset
         dataset = self.load_dataset(STATIC_FILES["chap2"])
@@ -283,13 +267,12 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 3
         assert scored < out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
-        assert all(r.name == "§2.3 Naming Conventions" for r in results)
+        assert all(r.name == u"§2.3 Naming Conventions" for r in results)
 
     def test_check_names_unique(self):
         """
         2.3 names should not be distinguished purely by case, i.e., if case is disregarded, no two names should be the same.
         """
-        # TEST CONFORMANCE 2.3 Recommended
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         result = self.cf.check_names_unique(dataset)
 
@@ -303,7 +286,7 @@ class TestCF1_6(BaseTestCase):
         assert result.value == (6, 7)
         assert (
             result.msgs[0]
-            == "Variables are not case sensitive. Duplicate variables named: not_unique"
+            == u"Variables are not case sensitive. Duplicate variables named: not_unique"
         )
 
     def test_check_dimension_names(self):
@@ -311,15 +294,13 @@ class TestCF1_6(BaseTestCase):
         2.4 A variable may have any number of dimensions, including zero, and the dimensions must all have different names.
         """
 
-        # TEST CONFORMANCE 2.4 REQUIRED
-
         dataset = self.load_dataset(STATIC_FILES["bad_data_type"])
         result = self.cf.check_dimension_names(dataset)
         assert result.value == (6, 7)
 
         dataset = self.load_dataset(STATIC_FILES["chap2"])
         result = self.cf.check_dimension_names(dataset)
-        assert result.msgs[0] == "no_reason has two or more dimensions named time"
+        assert result.msgs[0] == u"no_reason has two or more dimensions named time"
 
     def test_check_dimension_order(self):
         """
@@ -328,12 +309,11 @@ class TestCF1_6(BaseTestCase):
         then X in the CDL definition corresponding to the file. All other dimensions should, whenever possible, be placed to the
         left of the spatiotemporal dimensions.
         """
-        # TEST CONFORMANCE 2.4 RECOMMENDED
         dataset = self.load_dataset(STATIC_FILES["bad_data_type"])
         result = self.cf.check_dimension_order(dataset)
         assert result.value == (5, 6)
         assert result.msgs[0] == (
-            "really_bad's spatio-temporal dimensions are not in the "
+            u"really_bad's spatio-temporal dimensions are not in the "
             "recommended order T, Z, Y, X and/or further dimensions are not "
             "located left of T, Z, Y, X. The dimensions (and their guessed "
             "types) are latitude (Y), power (U) (with U: other/unknown; L: "
@@ -349,11 +329,11 @@ class TestCF1_6(BaseTestCase):
         """
         2.5.1 The _FillValue should be outside the range specified by valid_range (if used) for a variable.
         """
-        # TEST CONFORMANCE 2.5.1 REQUIRED
+
         dataset = self.load_dataset(STATIC_FILES["bad_data_type"])
         result = self.cf.check_fill_value_outside_valid_range(dataset)
         assert result.msgs[0] == (
-            "salinity:_FillValue (1.0) should be outside the "
+            u"salinity:_FillValue (1.0) should be outside the "
             "range specified by valid_min/valid_max (-10, 10)"
         )
 
@@ -361,7 +341,7 @@ class TestCF1_6(BaseTestCase):
         result = self.cf.check_fill_value_outside_valid_range(dataset)
         assert result.value == (1, 2)
         assert result.msgs[0] == (
-            "wind_speed:_FillValue (12.0) should be outside the "
+            u"wind_speed:_FillValue (12.0) should be outside the "
             "range specified by valid_min/valid_max (0.0, 20.0)"
         )
 
@@ -370,8 +350,6 @@ class TestCF1_6(BaseTestCase):
         §2.6.1 the NUG defined global attribute Conventions to the string value
         "CF-1.6"
         """
-        # TEST CONFORMANCE 2.6.1 REQUIRED
-        # Note: conformance doc for 1.6 mentions CF-1.5
         # :Conventions = "CF-1.6"
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         result = self.cf.check_conventions_version(dataset)
@@ -387,7 +365,7 @@ class TestCF1_6(BaseTestCase):
         result = self.cf.check_conventions_version(dataset)
         self.assertFalse(result.value)
         assert result.msgs[0] == (
-            "§2.6.1 Conventions global attribute does not contain " '"CF-1.6"'
+            u"§2.6.1 Conventions global attribute does not contain " '"CF-1.6"'
         )
 
     def test_check_convention_globals(self):
@@ -396,7 +374,6 @@ class TestCF1_6(BaseTestCase):
         properly (§2.6.2).
         """
 
-        # CONFORMANCE 2.6.2
         # check for pass
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         result = self.cf.check_convention_globals(dataset)
@@ -408,7 +385,7 @@ class TestCF1_6(BaseTestCase):
         assert result.value[0] != result.value[1]
         assert (
             result.msgs[0]
-            == "§2.6.2 global attribute title should exist and be a non-empty string"
+            == u"§2.6.2 global attribute title should exist and be a non-empty string"
         )
 
     def test_check_convention_possibly_var_attrs(self):
@@ -436,7 +413,7 @@ class TestCF1_6(BaseTestCase):
         assert result.value[0] != result.value[1]
         assert (
             result.msgs[0]
-            == "§2.6.2 references global attribute should be a non-empty string"
+            == u"§2.6.2 references global attribute should be a non-empty string"
         )
 
         # load bad_data_type.nc
@@ -450,7 +427,8 @@ class TestCF1_6(BaseTestCase):
 
         assert result.value[0] != result.value[1]
         assert (
-            result.msgs[0] == "§2.6.2 salinity:institution should be a non-empty string"
+            result.msgs[0]
+            == u"§2.6.2 salinity:institution should be a non-empty string"
         )
 
     def test_check_standard_name(self):
@@ -472,18 +450,7 @@ class TestCF1_6(BaseTestCase):
         # 9 vars checked, 8 fail
         assert len(results) == 9
         assert score < out_of
-        assert all(r.name == "§3.3 Standard Name" for r in results)
-
-        # check recommendations with a misspelled standard name
-        dataset = MockTimeSeries()
-        temperature = dataset.createVariable("temperature", "f8", ("time",))
-        temperature.units = "degree_C"
-        temperature.standard_name = "sea_water_temperatrue"
-        results = self.cf.check_standard_name(dataset)
-        score, out_of, messages = get_results(results)
-        # only check for recommendation substring as recommendations might
-        # vary with differing standard name tables
-        assert " Possible close match(es):" in messages[-1]
+        assert all(r.name == u"§3.3 Standard Name" for r in results)
 
         # load different ds --  ll vars pass this check
         dataset = self.load_dataset(STATIC_FILES["reduced_horizontal_grid"])
@@ -491,33 +458,6 @@ class TestCF1_6(BaseTestCase):
         score, out_of, messages = get_results(results)
         assert score == out_of
 
-        dataset = MockTimeSeries()
-        temperature = dataset.createVariable("temperature", "f8", ("time",))
-        temperature.standard_name = "sea_water_temperature"
-        temperature.ancillary_variables = "temperature_flag"
-
-        temperature_flag = dataset.createVariable("temperature_flag", "i2", ("time",))
-        # bad modifier
-        temperature_flag.standard_name = "sea_water_temperature status flag"
-        _, _, messages = get_results(self.cf.check_standard_name(dataset))
-        assert (
-            'Standard name modifier "status flag" for variable temperature_flag is not a valid modifier according to CF Appendix C'
-            in messages
-        )
-        # proper name, units supplied
-        temperature_flag.standard_name = "sea_water_temperature status_flag"
-        temperature_flag.units = "1"
-        _, _, messages = get_results(self.cf.check_standard_name(dataset))
-
-        # TEST CONFORMANCE 3 RECOMMENDED
-        # long_name or standard_name present
-        del temperature.standard_name
-        _, _, messages = get_results(self.cf.check_standard_name(dataset))
-        assert (
-            "Attribute long_name or/and standard_name is highly "
-            "recommended for variable temperature" in messages
-        )
-
     def test_cell_bounds(self):
         dataset = self.load_dataset(STATIC_FILES["grid-boundaries"])
         results = self.cf.check_cell_boundaries(dataset)
@@ -566,7 +506,7 @@ class TestCF1_6(BaseTestCase):
         dataset = self.load_dataset(STATIC_FILES["bad_cell_measure2"])
         results = self.cf.check_cell_measures(dataset)
         score, out_of, messages = get_results(results)
-        message = "Cell measure variable box_area referred to by PS is not present in dataset variables"
+        message = u"Cell measure variable box_area referred to by PS is not present in dataset variables"
         assert message in messages
 
     def test_climatology_cell_methods(self):
@@ -621,11 +561,9 @@ class TestCF1_6(BaseTestCase):
         bad_dim_ds.createVariable("clim_bounds", "f8", ("time"))
         temp.climatology = "clim_bounds"
         results = self.cf.check_climatological_statistics(bad_dim_ds)
-        assert results[1].value[0] < results[1].value[1]
-        assert (
-            results[1].msgs[0] == 'Climatology dimension "clim_bounds" '
-            "should only contain two elements"
-        )
+        assert results[0].value[0] < results[0].value[1]
+        assert (results[0].msgs[0] == 'Climatology dimension "clim_bounds" '
+                "should only contain two elements")
 
     def test_check_ancillary_variables(self):
         """
@@ -635,15 +573,15 @@ class TestCF1_6(BaseTestCase):
         dataset = self.load_dataset(STATIC_FILES["rutgers"])
         results = self.cf.check_ancillary_variables(dataset)
         result_dict = {result.name: result for result in results}
-        result = result_dict["§3.4 Ancillary Data"]
+        result = result_dict[u"§3.4 Ancillary Data"]
         assert result.value == (2, 2)
 
         dataset = self.load_dataset(STATIC_FILES["bad_reference"])
         results = self.cf.check_ancillary_variables(dataset)
         result_dict = {result.name: result for result in results}
-        result = result_dict["§3.4 Ancillary Data"]
+        result = result_dict[u"§3.4 Ancillary Data"]
         assert result.value == (1, 2)
-        assert "temp_qc is not a variable in this dataset" == result.msgs[0]
+        assert u"temp_qc is not a variable in this dataset" == result.msgs[0]
 
     def test_download_standard_name_table(self):
         """
@@ -696,12 +634,6 @@ class TestCF1_6(BaseTestCase):
         # only 4 variables in this dataset do not have perfect scores
         imperfect = [r.value for r in results if r.value[0] < r.value[1]]
         assert len(imperfect) == 4
-        dataset.variables["conductivity_qc"] = MockVariable(
-            dataset.variables["conductivity_qc"]
-        )
-        # Test with single element.  Will fail, but should not throw exception.
-        dataset.variables["conductivity_qc"].flag_values = np.array([1], dtype=np.int8)
-        results = self.cf.check_flags(dataset)
 
     def test_check_flag_masks(self):
         dataset = self.load_dataset(STATIC_FILES["ghrsst"])
@@ -711,38 +643,12 @@ class TestCF1_6(BaseTestCase):
         assert scored > 0
         assert scored == out_of
 
-        dataset = MockTimeSeries()
-        flags_var = dataset.createVariable("flags", "f8", ("time",))
-        flags_var.standard_name = "quality_flag"
-        flags_var.flag_meanings = "LAND"
-        flags_var.flag_values = [1]
-        # test single element
-        flags_var.flag_masks = np.array([1], dtype="i2")
-        results = self.cf.check_flags(dataset)
-        assert scored > 0 and scored == out_of
-        # TEST CONFORMANCE 3.5 REQUIRED 7/8
-        flags_var.flag_masks = np.array([0, 1], dtype="i2")
-        results = self.cf.check_flags(dataset)
-        score, out_of, messages = get_results(results)
-        assert (
-            "flag_masks for variable flags must not contain zero as an "
-            "element" in messages
-        )
-        # IMPLEMENTATION 3.5 REQUIRED 1/1
-        flags_var.flag_masks = np.array([1], dtype="i2")
-        flags_var.flag_values = np.array([2], dtype="i2")
-        results = self.cf.check_flags(dataset)
-        score, out_of, messages = get_results(results)
-        assert (
-            "flag masks and flag values for 'flags' combined don't equal "
-            "flag values" in messages
-        )
-
     def test_check_bad_units(self):
         """Load a dataset with units that are expected to fail (bad_units.nc).
         There are 6 variables in this dataset, three of which should give
         an error:
             - time, with units "s" (should be <units> since <epoch>)
+            - lat, with units "degrees_E" (should be degrees)
             - lev, with units "level" (deprecated)"""
 
         dataset = self.load_dataset(STATIC_FILES["2dim"])
@@ -760,10 +666,12 @@ class TestCF1_6(BaseTestCase):
         results_list = list(chain(*(r.msgs for r in all_results if r.msgs)))
 
         # check the results only have '§3.1 Units' as the header
-        assert all(r.name == "§3.1 Units" for r in all_results)
+        assert all(r.name == u"§3.1 Units" for r in all_results)
 
         # check that all the expected variables have been hit
-        assert all(any(s in msg for msg in results_list) for s in ["time", "lev"])
+        assert all(
+            any(s in msg for msg in results_list) for s in ["time", "lat", "lev"]
+        )
 
     def test_latitude(self):
         """
@@ -782,7 +690,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 12
         assert scored < out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 3
-        assert (r.name == "§4.1 Latitude Coordinate" for r in results)
+        assert (r.name == u"§4.1 Latitude Coordinate" for r in results)
 
         # check with another ds -- all 6 vars checked pass
         dataset = self.load_dataset(STATIC_FILES["rotated_pole_grid"])
@@ -790,7 +698,7 @@ class TestCF1_6(BaseTestCase):
         scored, out_of, messages = get_results(results)
         assert len(results) == 6
         assert scored == out_of
-        assert (r.name == "§4.1 Latitude Coordinate" for r in results)
+        assert (r.name == u"§4.1 Latitude Coordinate" for r in results)
 
         # hack to avoid writing to read-only file
         dataset.variables["rlat"] = MockVariable(dataset.variables["rlat"])
@@ -800,7 +708,7 @@ class TestCF1_6(BaseTestCase):
         rlat.units = "degrees_north"
         results = self.cf.check_latitude(dataset)
         scored, out_of, messages = get_results(results)
-        wrong_format = "Grid latitude variable '{}' should use degree equivalent units without east or north components. Current units are {}"
+        wrong_format = u"Grid latitude variable '{}' should use degree equivalent units without east or north components. Current units are {}"
         self.assertTrue(wrong_format.format(rlat.name, rlat.units) in messages)
         rlat.units = "radians"
         results = self.cf.check_latitude(dataset)
@@ -824,7 +732,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 12
         assert scored < out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 3
-        assert all(r.name == "§4.2 Longitude Coordinate" for r in results)
+        assert all(r.name == u"§4.2 Longitude Coordinate" for r in results)
 
         # check different dataset # TODO can be improved for check_latitude too
         dataset = self.load_dataset(STATIC_FILES["rotated_pole_grid"])
@@ -839,7 +747,7 @@ class TestCF1_6(BaseTestCase):
         rlon.units = "degrees_east"
         results = self.cf.check_longitude(dataset)
         scored, out_of, messages = get_results(results)
-        wrong_format = "Grid longitude variable '{}' should use degree equivalent units without east or north components. Current units are {}"
+        wrong_format = u"Grid longitude variable '{}' should use degree equivalent units without east or north components. Current units are {}"
         self.assertTrue(wrong_format.format(rlon.name, rlon.units) in messages)
         rlon.units = "radians"
         results = self.cf.check_longitude(dataset)
@@ -871,24 +779,12 @@ class TestCF1_6(BaseTestCase):
         units_set.units = "dbar"
         self.assertTrue(is_vertical_coordinate("not_known", units_set))
 
-        # TEST CONFORMANCE 4.3.2
         # Proper units/positive
         positive = mock_variable()
         positive.units = "m"
         positive.positive = "up"
         self.assertTrue(is_vertical_coordinate("not_known", positive))
 
-        # Proper units/negative
-        negative = mock_variable()
-        negative.units = "m"
-        negative.positive = "down"
-        self.assertTrue(is_vertical_coordinate("not_known", negative))
-
-        wrong = mock_variable()
-        wrong.units = "m"
-        wrong.positive = "left"
-        self.assertFalse(is_vertical_coordinate("not_known", wrong))
-
     def test_vertical_dimension(self):
         """
         Section 4.3.1 Dimensional Vertical Coordinate
@@ -897,14 +793,14 @@ class TestCF1_6(BaseTestCase):
         dataset = self.load_dataset(STATIC_FILES["example-grid"])
         results = self.cf.check_dimensional_vertical_coordinate(dataset)
         assert len(results) == 1
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
         # non-compliance -- one check fails
         dataset = self.load_dataset(STATIC_FILES["illegal-vertical"])
         results = self.cf.check_dimensional_vertical_coordinate(dataset)
         scored, out_of, messages = get_results(results)
         assert len(results) == 1
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
         assert scored < out_of
 
     def test_appendix_d(self):
@@ -1036,7 +932,7 @@ class TestCF1_6(BaseTestCase):
         # all variables checked (2) pass
         assert len(results) == 2
         assert scored == out_of
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
         # Check negative compliance -- 3 out of 4 pass
 
@@ -1046,7 +942,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 4
         assert scored <= out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
         # test with an invalid formula_terms
         dataset.variables["lev2"] = MockVariable(dataset.variables["lev2"])
@@ -1061,7 +957,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 4
         assert scored <= out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
     def test_is_time_variable(self):
         var1 = MockVariable()
@@ -1114,7 +1010,7 @@ class TestCF1_6(BaseTestCase):
 
         scored, out_of, messages = get_results(results)
 
-        assert "time does not have correct time units" in messages
+        assert u"time does not have correct time units" in messages
         assert (scored, out_of) == (1, 2)
 
     def test_check_calendar(self):
@@ -1129,71 +1025,17 @@ class TestCF1_6(BaseTestCase):
         dataset = self.load_dataset(STATIC_FILES["bad"])
         results = self.cf.check_calendar(dataset)
         scored, out_of, messages = get_results(results)
-        # TEST CONFORMANCE 4.4.1 REQUIRED 2, 3 / 5
-        bad_month_msg = (
-            "For nonstandard calendar on variable time, attribute "
-            "month_lengths must be supplied as a 12-element integer array"
-        )
-
-        assert bad_month_msg in messages
 
-        dataset = MockTimeSeries()
-        dataset.variables["time"]
-        dataset.variables["time"].calendar = "custom"
-        dataset.variables["time"].month_lengths = np.array([30.3], dtype=np.double)
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert bad_month_msg in messages
-
-        dataset.variables["time"].month_lengths = np.array(
-            [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31], dtype=np.int
-        )
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert bad_month_msg not in messages
-
-        # TEST CONFORMANCE 4.4.1 REQUIRED 4,5/5
-        leap_month_msg = (
-            "When attribute leap_month is supplied for variable "
-            "time, the value must be a scalar integer between 1 "
-            "and 12"
-        )
-        dataset.variables["time"].leap_month = np.array([0], dtype=np.uint8)
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert leap_month_msg in messages
-
-        dataset.variables["time"].leap_month = 2
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert leap_month_msg not in messages
-        # TEST CONFORMANCE 4.4.1 RECOMMENDED 1/2
         assert (
-            "For time variable time, attribute leap_year must be present "
-            "if leap_month attribute is defined" in messages
-        )
-
-        # TEST CONFORMANCE 4.4.1 REQUIRED 5/5
-        leap_year_msg = (
-            "When attribute leap_year is supplied for variable "
-            "time, the value must be a scalar integer"
+            u"§4.4.1 Variable time should have a valid calendar: 'nope' is not a valid calendar"
+            in messages
         )
 
-        dataset.variables["time"].leap_year = ["2.18"]
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert leap_year_msg in messages
-
-        dataset.variables["time"].leap_year = 4
-        results = self.cf.check_calendar(dataset)
-        scored, out_of, messages = get_results(results)
-        assert leap_year_msg not in messages
-
     def test_check_aux_coordinates(self):
         dataset = self.load_dataset(STATIC_FILES["illegal-aux-coords"])
         results = self.cf.check_aux_coordinates(dataset)
         result_dict = {result.name: result for result in results}
-        result = result_dict["§5 Coordinate Systems"]
+        result = result_dict[u"§5 Coordinate Systems"]
         assert result.msgs == []  # shouldn't have any messages
         assert result.value == (4, 4)
 
@@ -1204,7 +1046,7 @@ class TestCF1_6(BaseTestCase):
 
         result_dict = {result.name: result for result in results}
         result = result_dict[
-            "§5.6 Horizontal Coordinate Reference Systems, Grid Mappings, Projections"
+            u"§5.6 Horizontal Coordinate Reference Systems, Grid Mappings, Projections"
         ]
         assert result.value == (2, 2)
         assert (scored, out_of) == (2, 2)
@@ -1224,7 +1066,7 @@ class TestCF1_6(BaseTestCase):
         assert scored < out_of
         assert all(
             r.name
-            == "§5.6 Horizontal Coordinate Reference Systems, Grid Mappings, Projections"
+            == u"§5.6 Horizontal Coordinate Reference Systems, Grid Mappings, Projections"
             for r in results
         )
 
@@ -1234,7 +1076,7 @@ class TestCF1_6(BaseTestCase):
         scored, out_of, messages = get_results(results)
         assert scored == out_of
         assert len(results) == 1
-        assert all(r.name == "§5.3 Reduced Horizontal Grid" for r in results)
+        assert all(r.name == u"§5.3 Reduced Horizontal Grid" for r in results)
 
         # load failing ds -- one variable has failing check
         dataset = self.load_dataset(STATIC_FILES["bad-rhgrid"])
@@ -1243,7 +1085,7 @@ class TestCF1_6(BaseTestCase):
         assert scored != out_of
         assert len(results) == 2
         assert len([r for r in results if r.value[0] < r.value[1]]) == 1
-        assert all(r.name == "§5.3 Reduced Horizontal Grid" for r in results)
+        assert all(r.name == u"§5.3 Reduced Horizontal Grid" for r in results)
 
     def test_check_grid_mapping(self):
         dataset = self.load_dataset(STATIC_FILES["mapping"])
@@ -1391,7 +1233,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 2
         assert scored < out_of
         assert (
-            "6.1.1 'Neverland' specified by 'neverland' is not a valid region"
+            u"6.1.1 'Neverland' specified by 'neverland' is not a valid region"
             in messages
         )
 
@@ -1401,10 +1243,10 @@ class TestCF1_6(BaseTestCase):
         score, out_of, messages = get_results(results)
 
         msgs = [
-            "Type of tempvalid_min attribute (int32) does not match variable type (int64)",
-            "Type of temp:valid_max attribute (int32) does not match variable type (int64)",
-            "Type of salinityvalid_min attribute (int32) does not match variable type (float64)",
-            "Type of salinity:valid_max attribute (int32) does not match variable type (float64)",
+            u"Type of tempvalid_min attribute (int32) does not match variable type (int64)",
+            u"Type of temp:valid_max attribute (int32) does not match variable type (int64)",
+            u"Type of salinityvalid_min attribute (int32) does not match variable type (float64)",
+            u"Type of salinity:valid_max attribute (int32) does not match variable type (float64)",
         ]
 
         self.assertEqual(len(results), 4)
@@ -1469,37 +1311,10 @@ class TestCF1_6(BaseTestCase):
         #                 furthermore is fragile and breaks tests when check
         #                 definitions change
         scored, out_of, messages = get_results(results)
-        assert scored == out_of
+        assert scored == 24
+        assert out_of == 24
         assert messages == []
 
-    def test_check_standard_name_modifier_units(self):
-        """Test that standard name modifiers are properly processed"""
-        dataset = MockTimeSeries()
-
-        temp = dataset.createVariable("temperature", "f8", ("time",))
-        temp.standard_name = "sea_water_temperature"
-        temp.units = "degree_C"
-
-        temp_flag = dataset.createVariable("temp_flag", "i1", ("time",))
-        temp_flag.standard_name = "sea_water_temperature status_flag"
-        # units should not exist for
-        temp_flag.units = "1"
-
-        temp.ancillary_variables = "temp_flag"
-        scored, out_of, messages = get_results(self.cf.check_units(dataset))
-        assert scored != out_of
-        assert (
-            "units attribute for variable temperature_flag must be unset "
-            "when status_flag modifier is set"
-        )
-
-        del temp_flag.units
-        scored, out_of, messages = get_results(self.cf.check_units(dataset))
-        assert scored == out_of
-
-        dataset.createVariable("temp_counts", "i1", ("time",))
-        temp.ancillary_variables += " temp_counts"
-
     def test_check_duplicates(self):
         """
         Test to verify that the check identifies duplicate axes. Load the
@@ -1513,7 +1328,7 @@ class TestCF1_6(BaseTestCase):
 
         # only one check run here, so we can directly compare all the values
         assert scored != out_of
-        assert messages[0] == "'temp' has duplicate axis X defined by [lon_rho, lon_u]"
+        assert messages[0] == u"'temp' has duplicate axis X defined by [lon_rho, lon_u]"
 
     def test_check_multi_dimensional_coords(self):
         """
@@ -1527,7 +1342,7 @@ class TestCF1_6(BaseTestCase):
         # 4 variables were checked in this ds, 2 of which passed
         assert len(results) == 4
         assert len([r for r in results if r.value[0] < r.value[1]]) == 2
-        assert all(r.name == "§5 Coordinate Systems" for r in results)
+        assert all(r.name == u"§5 Coordinate Systems" for r in results)
 
     def test_64bit(self):
         dataset = self.load_dataset(STATIC_FILES["ints64"])
@@ -1544,7 +1359,7 @@ class TestCF1_6(BaseTestCase):
         assert len(results) == 2
         assert scored < out_of
         assert len([r for r in results if r.value[0] < r.value[1]]) == 1
-        assert all(r.name == "§9.1 Features and feature types" for r in results)
+        assert all(r.name == u"§9.1 Features and feature types" for r in results)
 
         # compliant dataset
         dataset = self.load_dataset(STATIC_FILES["trajectory-complete"])
@@ -1573,7 +1388,7 @@ class TestCF1_6(BaseTestCase):
 
         # check the results only have expected headers
         assert set([r.name for r in results]).issubset(
-            set(["§7.1 Cell Boundaries", "§7.3 Cell Methods"])
+            set([u"§7.1 Cell Boundaries", u"§7.3 Cell Methods"])
         )
 
         # check that all the expected variables have been hit
@@ -1605,7 +1420,7 @@ class TestCF1_6(BaseTestCase):
         scored, out_of, messages = get_results(results)
 
         self.assertTrue(
-            '§7.3.3 The non-standard "comment:" element must come after any standard elements in cell_methods for variable temperature'
+            u'§7.3.3 The non-standard "comment:" element must come after any standard elements in cell_methods for variable temperature'
             in messages
         )
 
@@ -1622,7 +1437,7 @@ class TestCF1_6(BaseTestCase):
         results = self.cf.check_cell_methods(nc_obj)
         scored, out_of, messages = get_results(results)
         self.assertTrue(
-            '§7.3.3 Invalid cell_methods keyword "invalid_keyword:" for variable temperature. Must be one of [interval, comment]'
+            u'§7.3.3 Invalid cell_methods keyword "invalid_keyword:" for variable temperature. Must be one of [interval, comment]'
             in messages
         )
 
@@ -1633,7 +1448,7 @@ class TestCF1_6(BaseTestCase):
         results = self.cf.check_cell_methods(nc_obj)
         scored, out_of, messages = get_results(results)
         assert (
-            "§7.3.3 Parenthetical content inside temperature:cell_methods is not well formed: interval 0.2 m interval: 0.01 degrees"
+            u"§7.3.3 Parenthetical content inside temperature:cell_methods is not well formed: interval 0.2 m interval: 0.01 degrees"
             in messages
         )
 
@@ -1659,28 +1474,6 @@ class TestCF1_7(BaseTestCase):
 
         self.cf = CF1_7Check()
 
-    def test_check_external_variables(self):
-        dataset = MockTimeSeries()
-        # bad type should be ignored here and instead handled by CF Appendix A
-        dataset.external_variables = 1
-        result = self.cf.check_external_variables(dataset)
-        assert result.value[0] == result.value[1] == 0
-        dataset.external_variables = "ext1 ext2 ext3"
-        result = self.cf.check_external_variables(dataset)
-        assert result.value[0] == result.value[1]
-        # TEST CONFORMANCE 2.6.3 REQUIRED 2/2
-        # dataset should not contain any external variables which are present in
-        # the dataset's variables
-        dataset.createVariable("ext3", "i4", ())
-        result = self.cf.check_external_variables(dataset)
-        assert result.value[0] < result.value[1]
-        assert (
-            "Global attribute external_variables should not have any "
-            "variable names which are present in the dataset. Currently, "
-            "the following names appear in both external_variables "
-            "and the dataset's variables: {'ext3'}" in result.msgs
-        )
-
     def test_check_actual_range(self):
         """Test the check_actual_range method works as expected"""
 
@@ -1704,7 +1497,7 @@ class TestCF1_7(BaseTestCase):
         score, out_of, messages = get_results(result)
         assert score < out_of
         assert len(messages) == 1
-        assert messages[0] == "actual_range of 'a' must be 2 elements"
+        assert messages[0] == u"actual_range of 'a' must be 2 elements"
         dataset.close()
 
         dataset = MockTimeSeries()
@@ -1803,9 +1596,7 @@ class TestCF1_7(BaseTestCase):
         dataset.close()
 
     def test_check_cell_boundaries(self):
-        """
-        Check our over-ridden check_cell_boundaries method behaves as expected
-        """
+        """Check our over-ridden check_cell_boundaries emthod behaves as expected"""
 
         dataset = self.load_dataset(STATIC_FILES["grid-boundaries"])
         results = self.cf.check_cell_boundaries(dataset)
@@ -1894,7 +1685,7 @@ class TestCF1_7(BaseTestCase):
         dataset = self.load_dataset(STATIC_FILES["bad_cell_measure2"])
         results = self.cf.check_cell_measures(dataset)
         score, out_of, messages = get_results(results)
-        message = "Cell measure variable box_area referred to by PS is not present in dataset variables"
+        message = u"Cell measure variable box_area referred to by PS is not present in dataset variables"
         assert message in messages
 
     def test_variable_features(self):
@@ -2092,18 +1883,6 @@ class TestCF1_7(BaseTestCase):
             result = self.cf.check_conventions_version(dataset)
             self.assertTrue(result.value)
 
-    def test_warn_on_deprecated_standard_name_modifiers(self):
-        ds = MockTimeSeries()
-        # normally, you'd set up a parent physical variable with
-        # ancillary_variables, but we only want to check bad standard name
-        # modifiers here
-        temp_counts = ds.createVariable("temp_counts", "i4", ("time",))
-        temp_counts.standard_name = "sea_water_temperature number_of_observations"
-        temp_flag = ds.createVariable("temp_flag", "i1", ("time",))
-        temp_flag.standard_name = "sea_water_temperature status_flag"
-        with pytest.warns(UserWarning):
-            self.cf.check_standard_name_deprecated_modifiers(ds)
-
     def test_appendix_d(self):
         """
         CF 1.7
@@ -2134,7 +1913,6 @@ class TestCF1_7(BaseTestCase):
         """
         Unit test for _check_dimensionless_vertical_coordinate_1_7 method.
         """
-        # IMPLEMENTATION 3.1 Recommended
         deprecated_units = ["level", "layer", "sigma_level"]
 
         ret_val = []
@@ -2166,7 +1944,7 @@ class TestCF1_7(BaseTestCase):
             assert score == 0
             assert out_of == 1
 
-            # this time, assign computed_standard_name
+            # this time, assign compufted_standard_name
             ret_val = []
             dataset.variables["lev"].setncattr("computed_standard_name", "air_pressure")
 
@@ -2198,7 +1976,7 @@ class TestCF1_7(BaseTestCase):
         # all variables checked (2) pass
         assert len(results) == 3
         assert scored == out_of
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
         # make one variable's computed_standard_name incorrect, one should fail
         dataset.variables["lev"].computed_standard_name = "definitely_not_right"
@@ -2207,7 +1985,7 @@ class TestCF1_7(BaseTestCase):
 
         assert len(results) == 3
         assert scored < out_of
-        assert all(r.name == "§4.3 Vertical Coordinate" for r in results)
+        assert all(r.name == u"§4.3 Vertical Coordinate" for r in results)
 
     def test_check_attr_type(self):
         """
@@ -2437,70 +2215,45 @@ class TestCF1_7(BaseTestCase):
         # set att bad (str)
         temp.setncattr("add_offset", "foo")
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertFalse(r[1].value)
+        self.assertFalse(r[0].value)
         # messages should be non-empty for improper type
-        self.assertTrue(r[1].msgs)
+        self.assertTrue(r[0].msgs)
         del temp.add_offset
 
         temp.setncattr("scale_factor", "foo")
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertFalse(r[1].value)
-        self.assertTrue(r[1].msgs)
+        self.assertFalse(r[0].value)
+        self.assertTrue(r[0].msgs)
 
         # set bad np val
         temp.setncattr("scale_factor", np.float32(5))
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertFalse(r[1].value)
-        self.assertTrue(r[1].msgs)
+        self.assertFalse(r[0].value)
+        self.assertTrue(r[0].msgs)
 
         temp.setncattr("scale_factor", np.uint(5))
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertFalse(r[1].value)
-        self.assertTrue(r[1].msgs)
+        self.assertFalse(r[0].value)
+        self.assertTrue(r[0].msgs)
 
         # set good
         temp.setncattr("scale_factor", np.float(5))
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertTrue(r[1].value)
-        self.assertFalse(r[1].msgs)
+        self.assertTrue(r[0].value)
+        self.assertFalse(r[0].msgs)
 
         temp.setncattr("scale_factor", np.double(5))
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertTrue(r[1].value)
-        self.assertFalse(r[1].msgs)
+        self.assertTrue(r[0].value)
+        self.assertFalse(r[0].msgs)
 
         # set same dtype
         dataset = MockTimeSeries()  # time lat lon depth
         temp = dataset.createVariable("temp", np.int, dimensions=("time",))
         temp.setncattr("scale_factor", np.int(5))
         r = self.cf.check_add_offset_scale_factor_type(dataset)
-        self.assertTrue(r[1].value)
-        self.assertFalse(r[1].msgs)
-
-        # integer variable type (int8, int16, int32) compared against
-        # floating point add_offset/scale_factor
-        for var_bytes in ("1", "2", "4"):
-            coarse_temp = dataset.createVariable(
-                f"coarse_temp_{var_bytes}", f"i{var_bytes}", dimensions=("time",)
-            )
-            coarse_temp.setncattr("scale_factor", np.float32(23.0))
-            coarse_temp.setncattr("add_offset", np.double(-2.1))
-            r = self.cf.check_add_offset_scale_factor_type(dataset)
-            # First value which checks if add_offset and scale_factor
-            # are same type should be false
-            self.assertFalse(r[0].value)
-            # TEST CONFORMANCE 8.1 REQUIRED 1/3
-            self.assertEqual(
-                r[0].msgs[0],
-                "When both scale_factor and add_offset are supplied for "
-                f"variable coarse_temp_{var_bytes}, they must have the "
-                "same type",
-            )
-            # Individual checks for scale_factor/add_offset should be OK,
-            # however
-            self.assertTrue(r[-1].value)
-            self.assertFalse(r[-1].msgs)
-            del dataset.variables[f"coarse_temp_{var_bytes}"]
+        self.assertTrue(r[0].value)
+        self.assertFalse(r[0].msgs)
 
 
 class TestCF1_8(BaseTestCase):
diff --git a/compliance_checker/tests/test_cf_integration.py b/compliance_checker/tests/test_cf_integration.py
index b6a01b9..eddc94f 100644
--- a/compliance_checker/tests/test_cf_integration.py
+++ b/compliance_checker/tests/test_cf_integration.py
@@ -3,6 +3,7 @@
 
 import pytest
 
+from netCDF4 import Dataset
 
 from compliance_checker.cf import util
 
@@ -20,15 +21,15 @@ dataset_stem__expected_messages = [
     (
         "sldmb_43093_agg",
         [
-            "attribute time:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
-            "attribute lat:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
-            "attribute lon:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
-            "§2.6.2 global attribute history should exist and be a non-empty string",
-            "standard_name temperature is not defined in Standard Name Table v{}".format(
+            u"attribute time:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
+            u"attribute lat:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
+            u"attribute lon:_CoordianteAxisType should begin with a letter and be composed of letters, digits, and underscores",
+            u"§2.6.2 global attribute history should exist and be a non-empty string",
+            u"standard_name temperature is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            "temperature's auxiliary coordinate specified by the coordinates attribute, precise_lat, is not a variable in this dataset",
-            "temperature's auxiliary coordinate specified by the coordinates attribute, precise_lon, is not a variable in this dataset",
+            u"temperature's auxiliary coordinate specified by the coordinates attribute, precise_lat, is not a variable in this dataset",
+            u"temperature's auxiliary coordinate specified by the coordinates attribute, precise_lon, is not a variable in this dataset",
         ],
     ),
     (
@@ -38,37 +39,29 @@ dataset_stem__expected_messages = [
     (
         "l01-met",
         [
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'air_temperature_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'barometric_pressure_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.float64'>) and parent variable 'wind_gust' (type: <class 'numpy.float32'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'wind_gust_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.float64'>) and parent variable 'wind_speed' (type: <class 'numpy.float32'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'wind_speed_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.float64'>) and parent variable 'wind_direction' (type: <class 'numpy.float32'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'wind_direction_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            "Attribute 'valid_range' (type: <class 'numpy.int16'>) and parent variable 'visibility_qc' (type: <class 'numpy.int8'>) must have equivalent datatypes",
-            '§2.6.1 Conventions global attribute does not contain "CF-1.8"',
+            "units for variable air_temperature_qc must be convertible to K currently they are 1",
+            "units for variable wind_speed_qc must be convertible to m s-1 currently they are 1",
             "standard_name visibility is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            'Standard name modifier "data_quality" for variable visibility_qc is not a valid modifier according to CF Appendix C',
+            "standard_name modifier data_quality for variable visibility_qc is not a valid modifier according to appendix C",
             "standard_name wind_direction is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            'Standard name modifier "data_quality" for variable wind_direction_qc is not a valid modifier according to CF Appendix C',
+            "standard_name modifier data_quality for variable wind_direction_qc is not a valid modifier according to appendix C",
             "standard_name wind_gust is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            'Standard name modifier "data_quality" for variable wind_gust_qc is not a valid modifier according to CF Appendix C',
-            'Standard name modifier "data_quality" for variable air_temperature_qc is not a valid modifier according to CF Appendix C',
+            "standard_name modifier data_quality for variable wind_gust_qc is not a valid modifier according to appendix C",
+            "standard_name modifier data_quality for variable air_temperature_qc is not a valid modifier according to appendix C",
             "standard_name use_wind is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
             "standard_name barometric_pressure is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            'Standard name modifier "data_quality" for variable barometric_pressure_qc is not a valid modifier according to CF Appendix C',
-            'Standard name modifier "data_quality" for variable wind_speed_qc is not a valid modifier according to CF Appendix C',
+            "standard_name modifier data_quality for variable barometric_pressure_qc is not a valid modifier according to appendix C",
+            "standard_name modifier data_quality for variable wind_speed_qc is not a valid modifier according to appendix C",
             "standard_name barometric_pressure is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
@@ -80,23 +73,24 @@ dataset_stem__expected_messages = [
     (
         "3mf07",
         [
-            "latitude:valid_min must be a numeric type not a string",
-            "latitude:valid_max must be a numeric type not a string",
-            "longitude:valid_min must be a numeric type not a string",
-            "longitude:valid_max must be a numeric type not a string",
-            "§2.6.2 references global attribute should be a non-empty string",
-            "§2.6.2 comment global attribute should be a non-empty string",
-            "dimensions for auxiliary coordinate variable z (z) are not a subset of dimensions for variable flag (profile)",
-            "dimensions for auxiliary coordinate variable z (z) are not a subset of dimensions for variable haul (profile)",
+            u"latitude:valid_min must be a numeric type not a string",
+            u"latitude:valid_max must be a numeric type not a string",
+            u"longitude:valid_min must be a numeric type not a string",
+            u"longitude:valid_max must be a numeric type not a string",
+            u"§2.6.2 references global attribute should be a non-empty string",
+            u"§2.6.2 comment global attribute should be a non-empty string",
+            u"dimensions for auxiliary coordinate variable z (z) are not a subset of dimensions for variable flag (profile)",
+            u"dimensions for auxiliary coordinate variable z (z) are not a subset of dimensions for variable haul (profile)",
         ],
     ),
     (
         "ooi_glider",
         [
-            "§2.6.2 comment global attribute should be a non-empty string",
-            "Attribute long_name or/and standard_name is highly recommended for variable deployment",
-            "latitude variable 'latitude' should define standard_name='latitude' or axis='Y'",
-            "longitude variable 'longitude' should define standard_name='longitude' or axis='X'",
+            u"§2.6.2 comment global attribute should be a non-empty string",
+            u"units (None) attribute of 'deployment' must be a string compatible with UDUNITS",
+            u"Attribute long_name or/and standard_name is highly recommended for variable deployment",
+            u"latitude variable 'latitude' should define standard_name='latitude' or axis='Y'",
+            u"longitude variable 'longitude' should define standard_name='longitude' or axis='X'",
         ],
     ),
     (
@@ -104,7 +98,8 @@ dataset_stem__expected_messages = [
         [
             "global attribute _CoordSysBuilder should begin with a letter and be composed of letters, digits, and underscores",
             '§2.6.1 Conventions global attribute does not contain "CF-1.8"',
-            'Units "hours since 2013-02-18T00:00:00Z" for variable time_offset must be convertible to canonical units "s"',
+            "units for variable time_offset must be convertible to s currently they are hours since 2013-02-18T00:00:00Z",
+            "units for variable time_run must be convertible to s currently they are hours since 2013-02-18 00:00:00.000 UTC",
             "lon's axis attribute must be T, X, Y, or Z, currently x",
             "lat's axis attribute must be T, X, Y, or Z, currently y",
             "z's axis attribute must be T, X, Y, or Z, currently z",
@@ -114,12 +109,11 @@ dataset_stem__expected_messages = [
     ),
     (
         "kibesillah",
-        ["§2.6.2 global attribute title should exist and be a non-empty string"],
+        [u"§2.6.2 global attribute title should exist and be a non-empty string"],
     ),
     (
         "pr_inundation",
         [
-            'Units "degrees2" for variable area must be convertible to canonical units "m2"',
             "waterlevel's spatio-temporal dimensions are not in the recommended order T, Z, Y, X and/or further dimensions are not located left of T, Z, Y, X. The dimensions (and their guessed types) are time (T), m (A), n (A) (with U: other/unknown; L: unlimited).",
             "velocity_x's spatio-temporal dimensions are not in the recommended order T, Z, Y, X and/or further dimensions are not located left of T, Z, Y, X. The dimensions (and their guessed types) are time (T), Layer (Z), m (A), n (A) (with U: other/unknown; L: unlimited).",
             "velocity_y's spatio-temporal dimensions are not in the recommended order T, Z, Y, X and/or further dimensions are not located left of T, Z, Y, X. The dimensions (and their guessed types) are time (T), Layer (Z), m (A), n (A) (with U: other/unknown; L: unlimited).",
@@ -129,7 +123,10 @@ dataset_stem__expected_messages = [
             "§2.6.2 depth:comment should be a non-empty string",
             "§2.6.2 institution global attribute should be a non-empty string",
             "§2.6.2 comment global attribute should be a non-empty string",
-            'Units "degrees2" for variable area must be convertible to canonical units "m2"',
+            "units (None) attribute of 'LayerInterf' must be a string compatible with UDUNITS",
+            "units (None) attribute of 'time_bounds' must be a string compatible with UDUNITS",
+            "units (None) attribute of 'Layer' must be a string compatible with UDUNITS",
+            "units for variable area must be convertible to m2 currently they are degrees2",
             "k: vertical coordinates not defining pressure must include a positive attribute that is either 'up' or 'down'",
             "grid_longitude has no coordinate associated with a variable identified as true latitude/longitude; its coordinate variable should also share a subset of grid_longitude's dimensions",
             "grid_latitude has no coordinate associated with a variable identified as true latitude/longitude; its coordinate variable should also share a subset of grid_latitude's dimensions",
@@ -139,45 +136,45 @@ dataset_stem__expected_messages = [
     (
         "ww3",
         [
-            "§2.6.2 global attribute title should exist and be a non-empty string",
-            "§2.6.2 global attribute history should exist and be a non-empty string",
-            "§2.6.1 Conventions field is not present",
-            "Attribute long_name or/and standard_name is highly recommended for variable time",
-            "Attribute long_name or/and standard_name is highly recommended for variable lon",
-            "Attribute long_name or/and standard_name is highly recommended for variable lat",
-            "latitude variable 'lat' should define standard_name='latitude' or axis='Y'",
-            "longitude variable 'lon' should define standard_name='longitude' or axis='X'",
+            u"§2.6.2 global attribute title should exist and be a non-empty string",
+            u"§2.6.2 global attribute history should exist and be a non-empty string",
+            u"§2.6.1 Conventions field is not present",
+            u"Attribute long_name or/and standard_name is highly recommended for variable time",
+            u"Attribute long_name or/and standard_name is highly recommended for variable lon",
+            u"Attribute long_name or/and standard_name is highly recommended for variable lat",
+            u"latitude variable 'lat' should define standard_name='latitude' or axis='Y'",
+            u"longitude variable 'lon' should define standard_name='longitude' or axis='X'",
         ],
     ),
     (
         "glcfs",
         [
             # TODO: referenced/relative time is treated like time units
-            'Units "hours since 2016-01-01T12:00:00Z" for variable time_offset must be convertible to canonical units "s"',
+            "units for variable time_offset must be convertible to s currently they are hours since 2016-01-01T12:00:00Z",
             "standard_name cloud_cover is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
-            "standard_name dew_point is not defined in Standard Name Table v{}".format(
+            u"standard_name dew_point is not defined in Standard Name Table v{}".format(
                 std_names._version
             ),
             (
-                "GRID is not a valid CF featureType. It must be one of point, timeseries, "
+                u"GRID is not a valid CF featureType. It must be one of point, timeseries, "
                 "trajectory, profile, timeseriesprofile, trajectoryprofile"
             ),
             (
-                "global attribute _CoordSysBuilder should begin with a letter and "
+                u"global attribute _CoordSysBuilder should begin with a letter and "
                 "be composed of letters, digits, and underscores"
             ),
-            'units for cl, "fraction" are not recognized by UDUNITS',
+            u'units for cl, "fraction" are not recognized by UDUNITS',
         ],
     ),
     (
         "bad_cf_role",
         [
-            "§2.6.2 global attribute title should exist and be a non-empty string",
-            "§2.6.2 global attribute history should exist and be a non-empty string",
-            "§2.6.1 Conventions field is not present",
-            "§9.5 The only acceptable values of cf_role for Discrete Geometry CF data sets are timeseries_id, profile_id, and trajectory_id",
+            u"§2.6.2 global attribute title should exist and be a non-empty string",
+            u"§2.6.2 global attribute history should exist and be a non-empty string",
+            u"§2.6.1 Conventions field is not present",
+            u"§9.5 The only acceptable values of cf_role for Discrete Geometry CF data sets are timeseries_id, profile_id, and trajectory_id",
         ],
     ),
     pytest.param(
@@ -201,6 +198,11 @@ dataset_stem__expected_messages = [
             "w's spatio-temporal dimensions are not in the recommended order T, Z, Y, X and/or further dimensions are not located left of T, Z, Y, X. The dimensions (and their guessed types) are ocean_time (T), s_w (Z), eta_rho (A), xi_rho (A) (with U: other/unknown; L: unlimited).",
             "zeta's spatio-temporal dimensions are not in the recommended order T, Z, Y, X and/or further dimensions are not located left of T, Z, Y, X. The dimensions (and their guessed types) are ocean_time (T), eta_rho (A), xi_rho (A) (with U: other/unknown; L: unlimited).",
             '§2.6.1 Conventions global attribute does not contain "CF-1.8"',
+            "units (None) attribute of 's_w' must be a string compatible with UDUNITS",
+            "units (None) attribute of 's_rho' must be a string compatible with UDUNITS",
+            "units (None) attribute of 'Cs_w' must be a string compatible with UDUNITS",
+            "units (None) attribute of 'user' must be a string compatible with UDUNITS",
+            "units (None) attribute of 'Cs_r' must be a string compatible with UDUNITS",
             "CF recommends latitude variable 'lat_rho' to use units degrees_north",
             "CF recommends latitude variable 'lat_u' to use units degrees_north",
             "CF recommends latitude variable 'lat_v' to use units degrees_north",
@@ -306,7 +308,7 @@ class TestCFIntegration:
         # it's not clear to me what this is supposed to be doing -- this else clause is outside of the if
         else:
             raise AssertionError(
-                '"dimensions for auxiliary coordinate variable siglay (node, siglay) '
+                u'"dimensions for auxiliary coordinate variable siglay (node, siglay) '
                 'are not a subset of dimensions for variable u (siglay, nele, time)"'
                 " not in messages"
             )
diff --git a/compliance_checker/tests/test_cli.py b/compliance_checker/tests/test_cli.py
index c12c3c1..7b57216 100644
--- a/compliance_checker/tests/test_cli.py
+++ b/compliance_checker/tests/test_cli.py
@@ -86,7 +86,6 @@ class TestCLI:
         # since SimpleNamespace is Python 3.3+ only
         CheckSuite.checkers.clear()
         # need to mock setuptools entrypoints here in order to load in checkers
-
         def checker_1():
             return Namespace(name="checker_1")
 
diff --git a/compliance_checker/tests/test_ioos_profile.py b/compliance_checker/tests/test_ioos_profile.py
index 2ecad34..fd23488 100644
--- a/compliance_checker/tests/test_ioos_profile.py
+++ b/compliance_checker/tests/test_ioos_profile.py
@@ -12,7 +12,8 @@ from compliance_checker.ioos import (
     NamingAuthorityValidator,
 )
 from compliance_checker.tests import BaseTestCase
-from compliance_checker.tests.helpers import MockNetCDF, MockTimeSeries
+from compliance_checker.tests.helpers import (MockTimeSeries, MockVariable,
+                                              MockNetCDF)
 from compliance_checker.tests.resources import STATIC_FILES
 from compliance_checker.tests.test_cf import get_results
 
@@ -415,6 +416,60 @@ class TestIOOS1_2(BaseTestCase):
         scored, out_of, messages = get_results(results)
         self.assertEqual(scored, out_of)
 
+    def test_check_contributor_role_and_vocabulary(self):
+        ds = MockTimeSeries()  # time, lat, lon, depth
+
+        # no contributor_role or vocab, fail both
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertFalse(all(r.value for r in results))
+
+        # bad contributor_role and vocab
+        ds.setncattr("contributor_role", "bad")
+        ds.setncattr("contributor_role_vocabulary", "bad")
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertFalse(all(r.value for r in results))
+
+        # good role, bad vocab
+        ds.setncattr("contributor_role", "contributor")
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertTrue(results[0].value)
+        self.assertEqual(results[0].msgs, [])
+        self.assertFalse(results[1].value)
+
+        # bad role, good vocab
+        ds.setncattr("contributor_role", "bad")
+        ds.setncattr(
+            "contributor_role_vocabulary",
+            "http://vocab.nerc.ac.uk/collection/G04/current/",
+        )
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertFalse(results[0].value)
+        self.assertTrue(results[1].value)
+        self.assertEqual(results[1].msgs, [])
+
+        # good role, good vocab
+        ds.setncattr("contributor_role", "contributor")
+        ds.setncattr(
+            "contributor_role_vocabulary",
+            "http://vocab.nerc.ac.uk/collection/G04/current/",
+        )
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertTrue(results[0].value)
+        self.assertEqual(results[0].msgs, [])
+        self.assertTrue(results[1].value)
+        self.assertEqual(results[1].msgs, [])
+
+        ds.setncattr("contributor_role", "resourceProvider")
+        ds.setncattr(
+            "contributor_role_vocabulary",
+            "https://www.ngdc.noaa.gov/wiki/index.php?title=ISO_19115_and_19115-2_CodeList_Dictionaries#CI_RoleCode",
+        )
+        results = self.ioos.check_contributor_role_and_vocabulary(ds)
+        self.assertTrue(results[0].value)
+        self.assertEqual(results[0].msgs, [])
+        self.assertTrue(results[1].value)
+        self.assertEqual(results[1].msgs, [])
+
     def test_check_creator_and_publisher_type(self):
         """
         Checks the creator_type and publisher_type global attributes with
@@ -600,20 +655,21 @@ class TestIOOS1_2(BaseTestCase):
 
     def test_check_standard_name(self):
         ds = MockTimeSeries()  # time, lat, lon, depth
-        # all standard names on variables
+
+        # no standard names
         results = self.ioos.check_standard_name(ds)
         scored, out_of, messages = get_results(results)
-        self.assertEqual(scored, out_of)
+        self.assertLess(scored, out_of)
 
-        # all standard names except for depth
-        del ds.variables["depth"].standard_name
+        # give standard names to all variables
+        ds.variables["time"].setncattr("standard_name", "time")
+        ds.variables["lon"].setncattr("standard_name", "longitude")
+        ds.variables["lat"].setncattr("standard_name", "latitude")
+        ds.variables["depth"].setncattr("standard_name", "depth")
         results = self.ioos.check_standard_name(ds)
         scored, out_of, messages = get_results(results)
-        self.assertLess(scored, out_of)
+        self.assertEqual(scored, out_of)
 
-        # have to recreate here or temperature gives KeyError despite appearing
-        # deleted -- why?
-        ds = MockTimeSeries()  # time, lat, lon, depth
         # add a QARTOD variable, no standard name - should fail
         qr = ds.createVariable("depth_qc", np.byte)
         qr.setncattr("flag_meanings", "blah")
@@ -753,11 +809,7 @@ class TestIOOS1_2(BaseTestCase):
         # QARTOD variable with flag meanings, without flag_meanings
         qr.setncattr("flag_values", np.array([0, 1, 2], dtype=np.byte))
         results = self.ioos.check_qartod_variables_flags(ds)
-        self.assertIn(
-            "Variable depth_qc must have attribute flag_meanings defined when flag_values attribute is present",
-            results[0].msgs,
-        )
-        self.assertNotEqual(results[0].value[0], results[0].value[1])  # should fail
+        self.assertEqual(results[0].value[0], results[0].value[1])  # should pass
         self.assertFalse(results[1].value)  # still fail
 
         # QARTOD variable with flag meanings, flag_values
@@ -945,7 +997,7 @@ class TestIOOS1_2(BaseTestCase):
 
     def test_check_feattype_timeseries_cf_role(self):
 
-        # featureType: timeseries and timeseries - msingle station require same tests
+        ### featureType: timeseries and timeseries - msingle station require same tests ###
 
         # for ftype in ("timeseries", "timeseries - single station", "timeseries - multiple station"):
         ftype = "timeseries"
diff --git a/compliance_checker/tests/test_protocols.py b/compliance_checker/tests/test_protocols.py
index 999c8d9..5143e26 100644
--- a/compliance_checker/tests/test_protocols.py
+++ b/compliance_checker/tests/test_protocols.py
@@ -36,7 +36,7 @@ class TestProtocols(TestCase):
         """
         Tests that a connection can be made to Hyrax
         """
-        url = "http://test.opendap.org:8080/opendap/ioos/mday_joinExist.ncml"
+        url = "http://ingria.coas.oregonstate.edu/opendap/hyrax/aggregated/ocean_time_aggregation.ncml"
         cs = CheckSuite()
         ds = cs.load_dataset(url)
         assert ds is not None
@@ -57,7 +57,7 @@ class TestProtocols(TestCase):
         """
         Tests that a connection can be made to an SOS endpoint
         """
-        url = "https://thredds.aoos.org/thredds/sos/aoos/cruises/ecofoci/2dy12.nc"
+        url = "https://data.oceansmap.com/thredds/sos/caricoos_ag/VIA/VIA.ncml"
         cs = CheckSuite()
         ds = cs.load_dataset(url)
         assert ds is not None
diff --git a/compliance_checker/tests/test_suite.py b/compliance_checker/tests/test_suite.py
index 325c1f8..701723c 100644
--- a/compliance_checker/tests/test_suite.py
+++ b/compliance_checker/tests/test_suite.py
@@ -64,11 +64,13 @@ class TestSuite(unittest.TestCase):
         ds = self.cs.load_dataset(static_files["2dim"])
         self.cs.run(ds, [], "acdd")
 
+
     def test_suite_pathlib(self):
         path_obj = Path(static_files["2dim"])
         ds = self.cs.load_dataset(path_obj)
         self.cs.run(ds, [], "acdd")
 
+
     def test_unicode_formatting(self):
         ds = self.cs.load_dataset(static_files["bad_region"])
         score_groups = self.cs.run(ds, [], "cf")
@@ -81,7 +83,8 @@ class TestSuite(unittest.TestCase):
             )
             # This asserts that print is able to generate all of the unicode
             # output
-            self.cs.standard_output_generation(groups, limit, points, out_of, checker)
+            self.cs.standard_output_generation(groups, limit, points, out_of,
+                                               checker)
 
     def test_generate_dataset_netCDF4(self):
         """
@@ -132,21 +135,21 @@ class TestSuite(unittest.TestCase):
         msg_set = {msg for sg in score_groups["cf"][0] for msg in sg.msgs}
 
         expected_excluded_names = {
-            "§3.5 flag_meanings for lat",
-            "§3.5 flag_meanings for lon",
-            "§3.5 lat is a valid flags variable",
-            "§3.5 lat is a valid flags variable",
-            "§3.5 lon is a valid flags variable",
+            u"§3.5 flag_meanings for lat",
+            u"§3.5 flag_meanings for lon",
+            u"§3.5 lat is a valid flags variable",
+            u"§3.5 lat is a valid flags variable",
+            u"§3.5 lon is a valid flags variable",
         }
 
         self.assertTrue(len(expected_excluded_names & name_set) == 0)
 
         # should skip references
-        ref_msg = "references global attribute should be a non-empty string"
+        ref_msg = u"references global attribute should be a non-empty string"
         self.assertTrue(ref_msg not in msg_set)
         # check_standard_name is high priority, but we requested only low,
         # so the standard_name check should still exist
-        standard_name_hdr = "§3.3 Standard Name"
+        standard_name_hdr = u"§3.3 Standard Name"
         self.assertTrue(standard_name_hdr in name_set)
 
     def test_group_func(self):