diff --git a/compliance_checker/acdd.py b/compliance_checker/acdd.py index c14545ae..831a4424 100644 --- a/compliance_checker/acdd.py +++ b/compliance_checker/acdd.py @@ -81,8 +81,11 @@ def __init__(self): # the method isn't executed repeatedly. self._applicable_variables = None + # to be used to format variable Result groups headers + self._var_header = "variable \"{}\" missing the following attributes:" + # set up attributes according to version - @check_has(BaseCheck.HIGH) + @check_has(BaseCheck.HIGH, gname="Global Attributes") def check_high(self, ds): ''' Performs a check on each highly recommended attributes' existence in the dataset @@ -91,7 +94,7 @@ def check_high(self, ds): ''' return self.high_rec_atts - @check_has(BaseCheck.MEDIUM) + @check_has(BaseCheck.MEDIUM, gname="Global Attributes") def check_recommended(self, ds): ''' Performs a check on each recommended attributes' existence in the dataset @@ -100,7 +103,7 @@ def check_recommended(self, ds): ''' return self.rec_atts - @check_has(BaseCheck.LOW) + @check_has(BaseCheck.LOW, gname="Global Attributes") def check_suggested(self, ds): ''' Performs a check on each suggested attributes' existence in the dataset @@ -120,17 +123,19 @@ def get_applicable_variables(self, ds): if self._applicable_variables is None: self.applicable_variables = cfutil.get_geophysical_variables(ds) varname = cfutil.get_time_variable(ds) - if varname: + # avoid duplicates by checking if already present + if varname and (varname not in self.applicable_variables): self.applicable_variables.append(varname) varname = cfutil.get_lon_variable(ds) - if varname: + if varname and (varname not in self.applicable_variables): self.applicable_variables.append(varname) varname = cfutil.get_lat_variable(ds) - if varname: + if varname and (varname not in self.applicable_variables): self.applicable_variables.append(varname) varname = cfutil.get_z_variable(ds) - if varname: + if varname and (varname not in self.applicable_variables): self.applicable_variables.append(varname) + return self.applicable_variables def check_var_long_name(self, ds): @@ -149,8 +154,8 @@ def check_var_long_name(self, ds): long_name = getattr(ds.variables[variable], 'long_name', None) check = long_name is not None if not check: - msgs.append("Var %s missing attribute long_name" % variable) - results.append(Result(BaseCheck.HIGH, check, "variable {}".format(variable), msgs)) + msgs.append("long_name") + results.append(Result(BaseCheck.HIGH, check, self._var_header.format(variable), msgs)) return results @@ -166,8 +171,8 @@ def check_var_standard_name(self, ds): std_name = getattr(ds.variables[variable], 'standard_name', None) check = std_name is not None if not check: - msgs.append("Var %s missing attribute standard_name" % variable) - results.append(Result(BaseCheck.HIGH, check, "variable {}".format(variable), msgs)) + msgs.append("standard_name") + results.append(Result(BaseCheck.HIGH, check, self._var_header.format(variable), msgs)) return results @@ -188,14 +193,16 @@ def check_var_units(self, ds): continue # Check if we have no units if not unit_check: - msgs.append("Var %s missing attribute units" % variable) - results.append(Result(BaseCheck.HIGH, unit_check, "variable {}".format(variable), msgs)) + msgs.append("units") + results.append(Result(BaseCheck.HIGH, unit_check, self._var_header.format(variable), msgs)) return results def check_acknowledgment(self, ds): ''' - Check if acknowledgment/acknowledgment attribute is present. + Check if acknowledgment/acknowledgment attribute is present. Because + acknowledgement has its own check, we are keeping it out of the Global + Attributes (even though it is a Global Attr). :param netCDF4.Dataset ds: An open netCDF dataset ''' @@ -204,9 +211,10 @@ def check_acknowledgment(self, ds): if hasattr(ds, 'acknowledgment') or hasattr(ds, 'acknowledgement'): check = True else: - messages.append("Attr acknowledgement not present") + messages.append("acknowledgment/acknowledgement not present") - return Result(BaseCheck.MEDIUM, check, 'acknowledgment/acknowledgement', msgs=messages) + # name="Global Attributes" so gets grouped with Global Attributes + return Result(BaseCheck.MEDIUM, check, "Global Attributes", msgs=messages) def check_lat_extents(self, ds): ''' @@ -346,8 +354,8 @@ def verify_geospatial_bounds(self, ds): check = var is not None if not check: return ratable_result(False, - 'geospatial_bounds', - ["Attr geospatial_bounds not present"]) + "Global Attributes", # grouped with Globals + ["geospatial_bounds not present"]) try: # TODO: verify that WKT is valid given CRS (defaults to EPSG:4326 @@ -355,11 +363,11 @@ def verify_geospatial_bounds(self, ds): from_wkt(ds.geospatial_bounds) except AttributeError: return ratable_result(False, - 'geospatial_bounds', + "Global Attributes", # grouped with Globals ['Could not parse WKT, possible bad value for WKT']) # parsed OK else: - return ratable_result(True, 'geospatial_bounds', tuple()) + return ratable_result(True, "Global Attributes", tuple()) def _check_total_z_extents(self, ds, z_variable): ''' @@ -516,19 +524,19 @@ def verify_convention_version(self, ds): """ Verify that the version in the Conventions field is correct """ - for convention in ds.Conventions.replace(' ', '').split(','): - if convention == 'ACDD-' + self._cc_spec_version: - return ratable_result((2, 2), - 'Conventions', - []) - # Conventions attribute is present, but does not include - # proper ACDD version - messages = [ - "Global Attribute 'Conventions' does not contain 'ACDD-{}'".format(self._cc_spec_version) - ] - return ratable_result((1, 2), - 'Conventions', - messages) + try: + for convention in getattr(ds, "Conventions", '').replace(' ', '').split(','): + if convention == 'ACDD-' + self._cc_spec_version: + return ratable_result((2, 2), None, []) # name=None so grouped with Globals + + # if no/wrong ACDD convention, return appropriate result + # Result will have name "Global Attributes" to group with globals + m = ["Conventions does not contain 'ACDD-{}'".format(self._cc_spec_version)] + return ratable_result((1, 2), "Global Attributes", m) + except AttributeError: # NetCDF attribute not found + m = ["No Conventions attribute present; must contain ACDD-{}".format(self._cc_spec_version)] + # Result will have name "Global Attributes" to group with globals + return ratable_result((0, 2), "Global Attributes", m) class ACDDNCCheck(BaseNCCheck, ACDDBaseCheck): @@ -567,7 +575,8 @@ def __init__(self): ('Conventions', self.verify_convention_version) ]) - self.rec_atts.extend(['geospatial_vertical_positive', + self.rec_atts.extend([ + 'geospatial_vertical_positive', 'geospatial_bounds_crs', 'geospatial_bounds_vertical_crs', 'publisher_name', # publisher,dataCenter @@ -689,11 +698,9 @@ def check_var_coverage_content_type(self, ds): 'coverage_content_type', None) check = ctype is not None if not check: - msgs.append("Var %s missing attribute coverage_content_type" % - variable) + msgs.append("coverage_content") results.append(Result(BaseCheck.HIGH, check, - "variable {}".format(variable), - msgs)) + self._var_header.format(variable), msgs)) continue # ISO 19115-1 codes @@ -708,7 +715,9 @@ def check_var_coverage_content_type(self, ds): 'coordinate' } if ctype not in valid_ctypes: - msgs.append("Var %s does not have a coverage_content_type in %s" + msgs.append("coverage_content_type in \"%s\"" % (variable, sorted(valid_ctypes))) + results.append(Result(BaseCheck.HIGH, check, # append to list + self._var_header.format(variable), msgs)) return results diff --git a/compliance_checker/base.py b/compliance_checker/base.py index 4839a38c..31cb2d27 100644 --- a/compliance_checker/base.py +++ b/compliance_checker/base.py @@ -231,13 +231,20 @@ def xpath_check(tree, xpath): return len(xpath(tree)) > 0 -def attr_check(l, ds, priority, ret_val): +def attr_check(l, ds, priority, ret_val, gname=None): """ Handles attribute checks for simple presence of an attribute, presence of one of several attributes, and passing a validation function. Returns a status along with an error message in the event of a failure. Mutates ret_val parameter + + :param tuple(str, func) or str l: the attribute being checked + :param netCDF4 dataset ds : dataset being checked + :param int priority : priority level of check + :param list ret_val : result to be returned + :param str or None gname : group name assigned to a group of attribute Results """ + msgs = [] if isinstance(l, tuple): name, other = l @@ -246,18 +253,32 @@ def attr_check(l, ds, priority, ret_val): # check instead res = std_check_in(ds, name, other) if res == 0: - msgs.append("Attr %s not present" % name) + msgs.append("%s not present" % name) elif res == 1: - msgs.append("Attr %s present, but not in expected value list (%s)" % (name, other)) - - ret_val.append(Result(priority, (res, 2), name, msgs)) + msgs.append("%s present, but not in expected value list (%s)" % (name, other)) + + ret_val.append( + Result( + priority, + (res, 2), + gname if gname else name, # groups Globals if supplied + msgs + ) + ) # if we have an XPath expression, call it on the document elif type(other) is etree.XPath: # TODO: store tree instead of creating it each time? res = xpath_check(ds._root, other) if not res: msgs = ["XPath for {} not found".format(name)] - ret_val.append(Result(priority, res, name, msgs)) + ret_val.append( + Result( + priority, + res, + gname if gname else name, + msgs + ) + ) # if the attribute is a function, call it # right now only supports single attribute # important note: current magic approach uses all functions @@ -270,10 +291,18 @@ def attr_check(l, ds, priority, ret_val): # to check whether the attribute is present every time # and instead focuses on the core functionality of the # test - res = std_check(ds, name) + + res = other(ds) # call the method on the dataset if not res: - msgs = ["Attr %s not present" % name] - ret_val.append(Result(priority, res, name, msgs)) + msgs = ["%s not present" % name] + ret_val.append( + Result( + priority, + res, + gname if gname else name, + msgs + ) + ) else: ret_val.append(other(ds)(priority)) # unsupported second type in second @@ -283,7 +312,7 @@ def attr_check(l, ds, priority, ret_val): else: res = std_check(ds, l) if not res: - msgs = ["Attr %s not present" % l] + msgs = ["%s not present" % l] else: try: # see if this attribute is a string, try stripping @@ -291,17 +320,25 @@ def attr_check(l, ds, priority, ret_val): att_strip = getattr(ds, l).strip() if not att_strip: res = False - msgs = ["Attr %s is empty or completely whitespace" % l] + msgs = ["%s is empty or completely whitespace" % l] # if not a string/has no strip method we should be OK except AttributeError: pass - ret_val.append(Result(priority, res, l, msgs)) + # gname arg allows the global attrs to be grouped together + ret_val.append(Result( + priority, + value=res, + name=gname if gname else l, + msgs=msgs + )) return ret_val -def check_has(priority=BaseCheck.HIGH): +def check_has(priority=BaseCheck.HIGH, gname=None): + """Decorator to wrap a function to check if a dataset has given attributes. + :param function func: function to wrap""" def _inner(func): def _dec(s, ds): @@ -312,7 +349,7 @@ def _dec(s, ds): # effects on `ret_val` for l in list_vars: # function mutates ret_val - attr_check(l, ds, priority, ret_val) + attr_check(l, ds, priority, ret_val, gname) return ret_val return wraps(func)(_dec) @@ -325,12 +362,12 @@ def fix_return_value(v, method_name, method=None, checker=None): Transforms scalar return values into Result. """ # remove common check prefix - method_name = (method_name or method.__func__.__name__).replace("check_", - "") + method_name = (method_name or method.__func__.__name__).replace("check_","") if v is None or not isinstance(v, Result): v = Result(value=v, name=method_name) v.name = v.name or method_name + v.checker = checker v.check_method = method diff --git a/compliance_checker/cf/cf.py b/compliance_checker/cf/cf.py index 834681c6..6961bb94 100644 --- a/compliance_checker/cf/cf.py +++ b/compliance_checker/cf/cf.py @@ -939,7 +939,7 @@ def check_units(self, ds): # side effects, but better than teasing out the individual result if units_attr_is_string.assert_true( isinstance(units, basestring), - "units ({}) attribute of '{}' must be a string".format(units, variable.name) + "units ({}) attribute of '{}' must be a string compatible with UDUNITS".format(units, variable.name) ): valid_udunits = self._check_valid_udunits(ds, name) ret_val.append(valid_udunits) @@ -1032,7 +1032,7 @@ def _check_valid_udunits(self, ds, variable_name): should_be_dimensionless = (variable.dtype.char == 'S' or std_name_units_dimensionless) - valid_udunits = TestCtx(BaseCheck.LOW, self.section_titles["3.1"]) + 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, 'units for {}, "{}" are not recognized by UDUNITS'.format(variable_name, units)) diff --git a/compliance_checker/runner.py b/compliance_checker/runner.py index b5512365..01011a57 100644 --- a/compliance_checker/runner.py +++ b/compliance_checker/runner.py @@ -75,6 +75,8 @@ def run_checker(cls, ds_loc, checker_names, verbose, criteria, else: score_dict[loc] = score_groups + # define a score limit to truncate the ouput to the strictness level + # specified by the user if criteria == 'normal': limit = 2 elif criteria == 'strict': diff --git a/compliance_checker/suite.py b/compliance_checker/suite.py index 2df8ca2a..1fb5f93c 100644 --- a/compliance_checker/suite.py +++ b/compliance_checker/suite.py @@ -264,8 +264,8 @@ def run(self, ds, skip_checks, *checker_names): for checker_name, checker_class in checkers: - checker = checker_class() - checker.setup(ds) + checker = checker_class() # instantiate a Checker object + checker.setup(ds) # setup method to prep checks = self._get_checks(checker, skip_check_dict) vals = [] @@ -458,6 +458,7 @@ def standard_output(self, ds, limit, check_name, groups): Returns the dataset needed for the verbose output, as well as the failure flags. """ + score_list, points, out_of = self.get_points(groups, limit) issue_count = out_of - points @@ -483,14 +484,19 @@ def standard_output_generation(self, groups, limit, points, out_of, check): Generates the Terminal Output ''' if points < out_of: - self.reasoning_routine(groups, 0, check, priority_flag=limit) + self.reasoning_routine(groups, check, priority_flag=limit) else: print("All tests passed!") - def reasoning_routine(self, groups, indent, check, line=True, priority_flag=3, + def reasoning_routine(self, groups, check, priority_flag=3, _top_level=True): """ print routine performed + @param list groups: the Result groups + @param str check: checker name + @param int priority_flag: indicates the weight of the groups + @param bool _top_level: indicates the level of the group so as to + print out the appropriate header string """ sort_fn = lambda x: x.weight @@ -503,21 +509,25 @@ def reasoning_routine(self, groups, indent, check, line=True, priority_flag=3, priorities = self.checkers[check]._cc_display_headers def process_table(res, check): + """Recursively calls reasoning_routine to parse out child reasons + from the parent reasons. + @param Result res: Result object + @param str check: checker name""" + issue = res.name if not res.children: reasons = res.msgs else: child_reasons = self.reasoning_routine(res.children, - indent + 1, - check, - _top_level=False) + check, _top_level=False) # there shouldn't be messages if there are children # is this a valid assumption? reasons = child_reasons return issue, reasons - # iterate up to the min priority requested + # iterate in reverse to the min priority requested; + # the higher the limit, the more lenient the output proc_strs = "" for level in range(3, priority_flag - 1, -1): level_name = priorities.get(level, level) @@ -546,7 +556,8 @@ def process_table(res, check): # separating this and the previous level if has_printed: print("") - reason_str = "\n".join('* {}'.format(r) for r in reasons) + # join alphabetized reasons together + reason_str = "\n".join('* {}'.format(r) for r in sorted(reasons, key=lambda x: x[0])) proc_str = "{}\n{}".format(issue, reason_str) print(proc_str) proc_strs.append(proc_str) @@ -647,21 +658,11 @@ def scores(self, raw_scores): def _group_raw(self, raw_scores, cur=None, level=1): """ Internal recursive method to group raw scores into a cascading score summary. - Only top level items are tallied for scores. + @param list raw_scores: list of raw scores (Result objects) """ - def build_group(label=None, weight=None, value=None, sub=None): - label = label - weight = weight - value = self._translate_value(value) - sub = sub or [] - - return Result(weight=weight, - value=value, - name=label, - children=sub) - + # BEGIN INTERNAL FUNCS ######################################## def trim_groups(r): if isinstance(r.name, tuple) or isinstance(r.name, list): new_name = r.name[1:] @@ -678,7 +679,12 @@ def trim_groups(r): def group_func(r): """ - Slices off first element (if list/tuple) of classification or just returns it if scalar. + Takes a Result object and slices off the first element of its name + if its's a tuple. Otherwise, does nothing to the name. Returns the + Result's name and weight in a tuple to be used for sorting in that + order in a groupby function. + @param Result r + @return tuple (str, int) """ if isinstance(r.name, tuple) or isinstance(r.name, list): if len(r.name) == 0: @@ -687,16 +693,28 @@ def group_func(r): retval = r.name[0:1][0] else: retval = r.name - return retval + return retval, r.weight + # END INTERNAL FUNCS ########################################## + + # NOTE until this point, *ALL* Results in raw_scores are + # individual Result objects. + # sort then group by name, then by priority weighting grouped = itertools.groupby(sorted(raw_scores, key=group_func), key=group_func) + # NOTE: post-grouping, grouped looks something like + # [(('Global Attributes', 1), ), + # (('Global Attributes', 3), ), + # (('Not a Global Attr', 1), )] + # (('Some Variable', 2), ), + ret_val = [] - for k, v in grouped: + for k, v in grouped: # iterate through the grouped tuples - v = list(v) + k = k[0] # slice ("name", weight_val) --> "name" + v = list(v) # from itertools._grouper to list cv = self._group_raw(list(map(trim_groups, v)), k, level + 1) if len(cv): @@ -704,6 +722,7 @@ def group_func(r): max_weight = max([x.weight for x in cv]) sum_scores = tuple(map(sum, list(zip(*([x.value for x in cv]))))) msgs = [] + else: max_weight = max([x.weight for x in v]) sum_scores = tuple(map(sum, list(zip(*([self._translate_value(x.value) for x in v]))))) diff --git a/compliance_checker/tests/data/bad_data_type.nc b/compliance_checker/tests/data/bad_data_type.nc index cec0b41a..a80e3891 100644 Binary files a/compliance_checker/tests/data/bad_data_type.nc and b/compliance_checker/tests/data/bad_data_type.nc differ diff --git a/compliance_checker/tests/test_acdd.py b/compliance_checker/tests/test_acdd.py index 0c37ca25..ff6dcfc2 100644 --- a/compliance_checker/tests/test_acdd.py +++ b/compliance_checker/tests/test_acdd.py @@ -27,7 +27,7 @@ def check_varset_nonintersect(group0, group1): class TestACDD1_1(BaseTestCase): - + # TODO superclass this so ACDD1_3 can inherit? # Adapted using `pandas.read_html` from URL # http://wiki.esipfed.org/index.php/Attribute_Convention_for_Data_Discovery_1-1 expected = { @@ -108,8 +108,12 @@ def test_highly_recommended(self): self.acdd_highly_recommended) # Check the reference dataset, NCEI 1.1 Gold Standard Point + missing = ['"Conventions" does not contain \'ACDD-1.3\''] results = self.acdd.check_high(self.ds) for result in results: + if result.msgs and all([m in missing for m in result.msgs]): + # only the Conventions check should have failed + self.assert_result_is_bad(result) self.assert_result_is_good(result) # Create an empty dataset that writes to /dev/null This acts as a @@ -130,20 +134,18 @@ def test_recommended(self): self.acdd_recommended) ncei_exceptions = [ - 'geospatial_bounds', - 'time_coverage_duration' + 'geospatial_bounds not present', + 'time_coverage_duration not present', + 'time_coverage_resolution not present' ] results = self.acdd.check_recommended(self.ds) for result in results: - # NODC 1.1 doesn't have some ACDD attributes - if result.name in ncei_exceptions: - continue - - # The NCEI Gold Standard Point is missing time_coverage_resolution... - if result.name == 'time_coverage_resolution': + if (result.msgs) and all([m in ncei_exceptions for m in result.msgs]): # we're doing string comparisons, this is kind of hacky... self.assert_result_is_bad(result) continue + self.assert_result_is_good(result) + # Create an empty dataset that writes to /dev/null This acts as a # temporary netCDF file in-memory that never gets written to disk. empty_ds = Dataset(os.devnull, 'w', diskless=True) @@ -162,14 +164,14 @@ def test_suggested(self): # Attributes that are missing from NCEI but should be there missing = [ - 'geospatial_lat_resolution', - 'geospatial_lon_resolution', - 'geospatial_vertical_resolution' + 'geospatial_lat_resolution not present', + 'geospatial_lon_resolution not present', + 'geospatial_vertical_resolution not present' ] results = self.acdd.check_suggested(self.ds) for result in results: - if result.name in missing: + if (result.msgs) and all([m in missing for m in result.msgs]): # we're doing string comparisons, this is kind of hacky... self.assert_result_is_bad(result) continue @@ -185,6 +187,7 @@ def test_suggested(self): self.assert_result_is_bad(result) def test_acknowldegement_check(self): + """Check both the British- and American-English spellings of 'acknowledgement'""" # Check British Spelling try: empty0 = Dataset(os.devnull, 'w', diskless=True) @@ -198,7 +201,7 @@ def test_acknowldegement_check(self): empty0.close() try: - # Check British spelling + # Check American spelling empty1 = Dataset(os.devnull, 'w', diskless=True) result = self.acdd.check_acknowledgment(empty1) self.assert_result_is_bad(result) @@ -287,6 +290,7 @@ class TestACDD1_3(BaseTestCase): ] } + def setUp(self): # Use the NCEI Gold Standard Point dataset for ACDD checks self.ds = self.load_dataset(STATIC_FILES['ncei_gold_point_2']) @@ -321,11 +325,11 @@ def test_recommended(self): results = self.acdd.check_recommended(self.ds) ncei_exceptions = [ - 'time_coverage_duration', - 'time_coverage_resolution' + 'time_coverage_duration not present', + 'time_coverage_resolution not present' ] for result in results: - if result.name in ncei_exceptions: + if (result.msgs) and all([m in ncei_exceptions for m in result.msgs]): # we're doing string comparisons, this is kind of hacky... self.assert_result_is_bad(result) continue @@ -341,12 +345,12 @@ def test_suggested(self): results = self.acdd.check_suggested(self.ds) # NCEI does not require or suggest resolution attributes ncei_exceptions = [ - 'geospatial_lat_resolution', - 'geospatial_lon_resolution', - 'geospatial_vertical_resolution' + 'geospatial_lat_resolution not present', + 'geospatial_lon_resolution not present', + 'geospatial_vertical_resolution not present' ] for result in results: - if result.name in ncei_exceptions: + if (result.msgs) and all([m in ncei_exceptions for m in result.msgs]): # we're doing string comparisons, this is kind of hacky... self.assert_result_is_bad(result) continue self.assert_result_is_good(result) @@ -395,20 +399,6 @@ def test_variables(self): for result in results: self.assert_result_is_bad(result) - def test_acknowledgement(self): - ''' - Test acknowledgement attribute is being checked - ''' - - empty_ds = Dataset(os.devnull, 'w', diskless=True) - self.addCleanup(empty_ds.close) - - result = self.acdd.check_acknowledgment(self.ds) - self.assert_result_is_good(result) - - result = self.acdd.check_acknowledgment(empty_ds) - self.assert_result_is_bad(result) - def test_vertical_extents(self): ''' Test vertical extents are being checked diff --git a/compliance_checker/tests/test_base.py b/compliance_checker/tests/test_base.py index 550f64a6..87863bd0 100644 --- a/compliance_checker/tests/test_base.py +++ b/compliance_checker/tests/test_base.py @@ -27,22 +27,22 @@ def test_attr_presence(self): rv1, rv2, rv3, rv4 = [], [], [], [] attr = 'test' base.attr_check(attr, self.ds, priority, rv1) - assert rv1[0] == base.Result(priority, False, 'test', - ['Attr test not present']) + assert rv1[0] == base.Result(priority, False, "test", + ['test not present']) # test with empty string self.ds.test = '' base.attr_check(attr, self.ds, priority, rv2) - assert rv2[0] == base.Result(priority, False, 'test', - ["Attr test is empty or completely whitespace"]) + assert rv2[0] == base.Result(priority, False, "test", + ["test is empty or completely whitespace"]) # test with whitespace in the form of a space and a tab self.ds.test = ' ' base.attr_check(attr, self.ds, priority, rv3) - assert rv3[0] == base.Result(priority, False, 'test', - ["Attr test is empty or completely whitespace"]) + assert rv3[0] == base.Result(priority, False, "test", + ["test is empty or completely whitespace"]) # test with actual string contents self.ds.test = 'abc 123' base.attr_check(attr, self.ds, priority, rv4) - assert rv4[0] == base.Result(priority, True, 'test', []) + assert rv4[0] == base.Result(priority, True, "test", []) def test_attr_in_valid_choices(self): """Tests attribute membership in a set""" @@ -51,33 +51,37 @@ def test_attr_in_valid_choices(self): valid_choices = ['a', 'b', 'c'] attr = ('test', valid_choices) base.attr_check(attr, self.ds, priority, rv1) - assert rv1[0] == base.Result(priority, (0, 2), 'test', ["Attr test not present"]) + assert rv1[0] == base.Result(priority, (0, 2), "test", ["test not present"]) self.ds.test = '' base.attr_check(attr, self.ds, priority, rv2) - assert rv2[0] == base.Result(priority, (1, 2), 'test', ["Attr test present, but not in expected value list (%s)" % valid_choices]) + assert rv2[0] == base.Result(priority, (1, 2), "test", ["test present, but not in expected value list (%s)" % valid_choices]) self.ds.test = 'a' base.attr_check(attr, self.ds, priority, rv3) - assert rv3[0] == base.Result(priority, (2, 2), 'test', []) + assert rv3[0] == base.Result(priority, (2, 2), "test", []) def test_attr_fn(self): """Test attribute against a checker function""" + # simple test. In an actual program, this use case would be covered rv1, rv2, rv3 = [], [], [] priority = base.BaseCheck.MEDIUM def verify_dummy(ds): - if ds.dummy + 'y' == 'dummyy': - return base.ratable_result(True, 'dummy', []) - else: - return base.ratable_result(False, 'dummy', ['not "dummyy"']) + """Sample function that will be called when passed into attr_check""" + try: + if ds.dummy + 'y' == 'dummyy': + return base.ratable_result(True, 'dummy', []) + else: + return base.ratable_result(False, 'dummy', [ds.dummy+'y']) + except AttributeError: + return base.ratable_result(False, 'dummy', []) attr = ('dummy', verify_dummy) base.attr_check(attr, self.ds, priority, rv1) - assert rv1[0] == base.Result(priority, False, 'dummy', - ['Attr dummy not present']) + assert rv1[0] == base.Result(priority, False, 'dummy', []) self.ds.dummy = 'doomy' base.attr_check(attr, self.ds, priority, rv2) - assert rv2[0] == base.Result(priority, False, 'dummy', ['not "dummyy"']) + assert rv2[0] == base.Result(priority, False, 'dummy', ['doomyy']) self.ds.dummy = 'dummy' base.attr_check(attr, self.ds, priority, rv3) assert rv3[0] == base.Result(priority, True, 'dummy', []) diff --git a/compliance_checker/tests/test_cf.py b/compliance_checker/tests/test_cf.py index e185a29f..3e416821 100644 --- a/compliance_checker/tests/test_cf.py +++ b/compliance_checker/tests/test_cf.py @@ -839,7 +839,7 @@ def test_check_packed_data(self): results = self.cf.check_packed_data(dataset) self.assertEqual(len(results), 4) self.assertFalse(results[0].value) - self.assertTrue(results[1].value) + self.assertFalse(results[1].value) self.assertTrue(results[2].value) self.assertFalse(results[3].value) diff --git a/compliance_checker/tests/test_cf_integration.py b/compliance_checker/tests/test_cf_integration.py index f4eadc59..83a7a129 100644 --- a/compliance_checker/tests/test_cf_integration.py +++ b/compliance_checker/tests/test_cf_integration.py @@ -119,11 +119,11 @@ def test_ocos(self): u"lwrad's dimensions are not in the recommended order T, Z, Y, X. They are ocean_time, eta_rho, xi_rho", u"swrad's dimensions are not in the recommended order T, Z, Y, X. They are ocean_time, eta_rho, xi_rho", u'§2.6.1 Conventions global attribute does not contain "CF-1.6". The CF Checker only supports CF-1.6 at this time.', - u"'units' attribute of 's_w' must be a string compatible with UDUNITS", - u"'units' attribute of 's_rho' must be a string compatible with UDUNITS", - u"'units' attribute of 'Cs_w' must be a string compatible with UDUNITS", - u"'units' attribute of 'user' must be a string compatible with UDUNITS", - u"'units' attribute of 'Cs_r' must be a string compatible with UDUNITS", + u"units (None) attribute of 's_w' must be a string compatible with UDUNITS", + u"units (None) attribute of 's_rho' must be a string compatible with UDUNITS", + u"units (None) attribute of 'Cs_w' must be a string compatible with UDUNITS", + u"units (None) attribute of 'user' must be a string compatible with UDUNITS", + u"units (None) attribute of 'Cs_r' must be a string compatible with UDUNITS", u"CF recommends latitude variable 'lat_rho' to use units degrees_north", u"CF recommends latitude variable 'lat_u' to use units degrees_north", u"CF recommends latitude variable 'lat_v' to use units degrees_north", @@ -165,7 +165,7 @@ def test_ocos(self): u'Unidentifiable feature for variable Cs_w', u'Unidentifiable feature for variable user' ] - assert all(m in messages for m in msgs) + assert all([m in messages for m in msgs]) def test_l01_met(self): @@ -266,7 +266,7 @@ def test_ooi_glider(self): msgs = [ u'§2.6.2 comment global attribute should be a non-empty string', - u"'units' attribute of 'deployment' must be a string compatible with UDUNITS", + 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'" @@ -323,9 +323,9 @@ def test_pr_inundation(self): u'§2.6.2 depth:comment should be a non-empty string', u'§2.6.2 institution global attribute should be a non-empty string', u'§2.6.2 comment global attribute should be a non-empty string', - u"'units' attribute of 'LayerInterf' must be a string compatible with UDUNITS", - u"'units' attribute of 'time_bounds' must be a string compatible with UDUNITS", - u"'units' attribute of 'Layer' must be a string compatible with UDUNITS", + u"units (None) attribute of 'LayerInterf' must be a string compatible with UDUNITS", + u"units (None) attribute of 'time_bounds' must be a string compatible with UDUNITS", + u"units (None) attribute of 'Layer' must be a string compatible with UDUNITS", u'units for variable area must be convertible to m2 currently they are degrees2', u"k: vertical coordinates not defining pressure must include a positive attribute that is either 'up' or 'down'", u'grid_longitude is not associated with a coordinate defining true latitude and sharing a subset of dimensions', @@ -336,7 +336,6 @@ def test_pr_inundation(self): u'Unidentifiable feature for variable time_bounds', u'Unidentifiable feature for variable grid_depth' ] - assert all(m in messages for m in msgs) @@ -392,15 +391,12 @@ def test_glcfs(self): "they are hours since 2016-01-01T12:00:00Z") in messages assert (u"standard_name cloud_cover is not defined in Standard Name Table v{}".format(self._std_names._version)) in messages assert (u"standard_name dew_point is not defined in Standard Name Table v{}".format(self._std_names._version)) in messages - # NOTE this dataset does not contain any variables with attribute 'bounds' - # assert (u"variable eta referenced by formula_terms does not exist") in messages - # assert (u"Boundary variable eta referenced by formula_terms not found in dataset variables") in messages assert (u"GRID is not a valid CF featureType. It must be one of point, timeseries, " "trajectory, profile, timeseriesprofile, trajectoryprofile") in messages assert (u"global attribute _CoordSysBuilder should begin with a letter and " "be composed of letters, digits, and underscores") in messages assert (u"source should be defined") - assert (u'units for cl, "fraction" are not recognized by udunits') in messages + assert (u'units for cl, "fraction" are not recognized by UDUNITS') in messages def test_ncei_templates(self): """