diff --git a/.gitignore b/.gitignore index 6569b199cf3..9aee52062bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ data out gitout +.coverage helpers/ckan/ helpers/ckan.json diff --git a/stats/dashboard.py b/stats/dashboard.py index cbaec079a6a..ae037f0d5da 100644 --- a/stats/dashboard.py +++ b/stats/dashboard.py @@ -358,6 +358,7 @@ def element_versions(self): @returns_numberdict @memoize def _major_version(self): + # TODO: Refactor to use _version parent = self.element.getparent() if parent is None: print('No parent of iati-activity, is this a test? Assuming version 1.xx') @@ -368,6 +369,20 @@ def _major_version(self): else: return '1' + @returns_numberdict + @memoize + def _version(self): + allowed_versions = CODELISTS['2']['Version'] + parent = self.element.getparent() + if parent is None: + print('No parent of iati-activity, is this a test? Assuming version 1.01') + return '1.01' + version = self.element.getparent().attrib.get('version') + if version and version in allowed_versions: + return version + else: + return '1.01' + @returns_numberdict def ruleset_passes(self): out = {} @@ -1172,6 +1187,41 @@ def comprehensiveness_denominators(self): 'transaction_traceability': 0 } + @returns_numberdict + def humanitarian(self): + humanitarian_sectors_dac_5_digit = ['72010', '72040', '72050', '73010', '74010'] + humanitarian_sectors_dac_3_digit = ['720', '730', '740'] + + # logic around use of the @humanitarian attribute + is_humanitarian_by_attrib_activity = 1 if ('humanitarian' in self.element.attrib) and (self.element.attrib['humanitarian'] in ['1', 'true']) else 0 + is_not_humanitarian_by_attrib_activity = 1 if ('humanitarian' in self.element.attrib) and (self.element.attrib['humanitarian'] in ['0', 'false']) else 0 + is_humanitarian_by_attrib_transaction = 1 if set(self.element.xpath('transaction/@humanitarian')).intersection(['1', 'true']) else 0 + is_not_humanitarian_by_attrib_transaction = 1 if not is_humanitarian_by_attrib_transaction and set(self.element.xpath('transaction/@humanitarian')).intersection(['0', 'false']) else 0 + is_humanitarian_by_attrib = (self._version() in ['2.02']) and (is_humanitarian_by_attrib_activity or (is_humanitarian_by_attrib_transaction and not is_not_humanitarian_by_attrib_activity)) + + # logic around DAC sector codes deemed to be humanitarian + is_humanitarian_by_sector_5_digit_activity = 1 if set(self.element.xpath('sector[@vocabulary="{0}" or not(@vocabulary)]/@code'.format(self._dac_5_code()))).intersection(humanitarian_sectors_dac_5_digit) else 0 + is_humanitarian_by_sector_5_digit_transaction = 1 if set(self.element.xpath('transaction[not(@humanitarian="0" or @humanitarian="false")]/sector[@vocabulary="{0}" or not(@vocabulary)]/@code'.format(self._dac_5_code()))).intersection(humanitarian_sectors_dac_5_digit) else 0 + is_humanitarian_by_sector_3_digit_activity = 1 if set(self.element.xpath('sector[@vocabulary="{0}"]/@code'.format(self._dac_3_code()))).intersection(humanitarian_sectors_dac_3_digit) else 0 + is_humanitarian_by_sector_3_digit_transaction = 1 if set(self.element.xpath('transaction[not(@humanitarian="0" or @humanitarian="false")]/sector[@vocabulary="{0}"]/@code'.format(self._dac_3_code()))).intersection(humanitarian_sectors_dac_3_digit) else 0 + # helper variables to help make logic easier to read + is_humanitarian_by_sector_activity = is_humanitarian_by_sector_5_digit_activity or is_humanitarian_by_sector_3_digit_activity + is_humanitarian_by_sector_transaction = is_humanitarian_by_sector_5_digit_transaction or is_humanitarian_by_sector_3_digit_transaction + is_humanitarian_by_sector = is_humanitarian_by_sector_activity or (is_humanitarian_by_sector_transaction and (self._major_version() in ['2'])) + + # combine the various ways in which an activity may be humanitarian + is_humanitarian = 1 if (is_humanitarian_by_attrib or is_humanitarian_by_sector) else 0 + # deal with some edge cases that have veto + if is_not_humanitarian_by_attrib_activity: + is_humanitarian = 0 + + return { + 'is_humanitarian': is_humanitarian, + 'is_humanitarian_by_attrib': is_humanitarian_by_attrib, + 'contains_humanitarian_scope': 1 if (self._version() in ['2.02']) and self.element.xpath('humanitarian-scope/@type') and self.element.xpath('humanitarian-scope/@code') else 0, + 'uses_humanitarian_clusters_vocab': 1 if (self._version() in ['2.02']) and self.element.xpath('sector/@vocabulary="10"') else 0 + } + def _transaction_type_code(self, transaction): type_code = None transaction_type = transaction.find('transaction-type') @@ -1285,8 +1335,6 @@ def sum_planned_disbursements_by_year(self): return out - -import json ckan = json.load(open('helpers/ckan.json')) publisher_re = re.compile('(.*)\-[^\-]') diff --git a/stats/tests/test_generic_file_stats.py b/stats/tests/test_generic_file_stats.py new file mode 100644 index 00000000000..b2400c3576b --- /dev/null +++ b/stats/tests/test_generic_file_stats.py @@ -0,0 +1,80 @@ +# coding=utf-8 + +from lxml import etree +import pytest + +from stats.dashboard import ActivityStats + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', '2.02']) +def test_version_detection_valid(version): + """ + Tests that valid versions of the IATI Standard are detected. + """ + + activity = ActivityStats() + + tree = etree.fromstring(''' + + + + + '''.format(version)) + + activity.element = tree.getchildren()[0] + + assert activity._version() == version + + +@pytest.mark.parametrize('version', ['1.06', '2.03', '3.01', '1', '', 'version 1.03', '1.03 version', '1.03 or 1.04', ' 2.01', '2 .01']) +def test_version_detection_invalid(version): + """ + Tests that invalid versions of the IATI Standard are detected. + """ + + activity = ActivityStats() + + tree = etree.fromstring(''' + + + + + '''.format(version)) + + activity.element = tree.getchildren()[0] + + assert activity._version() == '1.01' + + +def test_version_detection_no_parent(): + """ + Tests that XML with no parent returns default version. + """ + + activity = ActivityStats() + + activity.element = etree.fromstring(''' + + + ''') + + assert activity._version() == '1.01' + + +def test_version_detection_no_version_attrib(): + """ + Tests that XML with no version attribute returns default version. + """ + + activity = ActivityStats() + + tree = etree.fromstring(''' + + + + + ''') + + activity.element = tree.getchildren()[0] + + assert activity._version() == '1.01' diff --git a/stats/tests/test_humanitarian.py b/stats/tests/test_humanitarian.py new file mode 100644 index 00000000000..4f5d5da4617 --- /dev/null +++ b/stats/tests/test_humanitarian.py @@ -0,0 +1,684 @@ +# coding=utf-8 +from lxml import etree +import pytest + +from stats.dashboard import ActivityStats + +class MockActivityStats(ActivityStats): + def __init__(self, version): + if len(version) == 1 or len(version.split('.')) < 2: + self.major_version = version + self.minor_version = '02' + else: + self.major_version = version.split('.')[0] + self.minor_version = version.split('.')[1] + return super(MockActivityStats, self).__init__() + + def _major_version(self): + return self.major_version + + def _minor_version(self): + return self.minor_version + + def _version(self): + return self._major_version() + '.' + self._minor_version() + +HUMANITARIAN_SECTOR_CODES_5_DIGITS = [72010, 72040, 72050, 73010, 74010] +HUMANITARIAN_SECTOR_CODES_3_DIGITS = [720, 730, 740] + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('hum_attrib_val_true', ['1', 'true']) +@pytest.mark.parametrize('hum_attrib_val_false', ['0', 'false', 'True', 'False', '']) +@pytest.mark.parametrize('xml', [''' + + + + ''', ''' + + + + + ''', ''' + + + + + ''', ''' + + + + + ''', ''' + + + + + + ''']) +def test_humanitarian_attrib_true(version, hum_attrib_val_true, hum_attrib_val_false, xml): + """ + Detect an activity to be humanitarian using @humanitarian values that evaluate to true. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val_true, hum_attrib_val_false)) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 1 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', 'unknown version']) +@pytest.mark.parametrize('hum_attrib_val_true', ['1', 'true']) +@pytest.mark.parametrize('hum_attrib_val_false', ['0', 'false', 'True', 'False', '']) +@pytest.mark.parametrize('xml', [''' + + + + ''', ''' + + + + + ''', ''' + + + + + ''', ''' + + + + + ''', ''' + + + + + ''', ''' + + + + + + ''']) +def test_humanitarian_attrib_true_invalid_version(version, hum_attrib_val_true, hum_attrib_val_false, xml): + """ + Detect an activity to be humanitarian using @humanitarian values that evaluate to true, but at a version of the standard that does not support the attribute. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val_true, hum_attrib_val_false)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('hum_attrib_val', ['0', 'false']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + ''', ''' + + + + ''', ''' + + + + ''', ''' + + + + ''', ''' + + + + + + ''', ''' + + + + + + ''', ''' + + + + + ''']) +def test_humanitarian_attrib_false(version, hum_attrib_val, sector, xml): + """ + Detect an activity to not be humanitarian using @humanitarian values that evaluate to false. + + If there is a sector generally deemed to be humanitarian, the attribute shall take precedence. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val, sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('hum_attrib_val', ['True', 'False', '']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + + ''', ''' + + + + + + ''', ''' + + + + + + ''']) +def test_humanitarian_attrib_invalid_sector(version, hum_attrib_val, sector, xml): + """ + Detect an activity to not be humanitarian using @humanitarian values that evaluate to false. + + If there is a sector generally deemed to be humanitarian, the attribute shall take precedence. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val, sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('hum_attrib_val', ['True', 'False', '']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + ''', ''' + + + + ''', ''' + + + + ''']) +def test_humanitarian_attrib_invalid_no_sector(version, hum_attrib_val, sector, xml): + """ + Detect an activity to not be humanitarian using @humanitarian values that evaluate to false. + + If there is a sector generally deemed to be humanitarian, the attribute shall take precedence. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val, sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['1', '2']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + + + ''', ''' + + + + + ''', ''' + + + + + + + + ''']) +def test_humanitarian_sector_true(major_version, sector, xml): + """ + Detects an activity to be humanitarian using sector codes considered to relate to humanitarian. + + Also checks that the appropriate vocabulary is provided or assumed. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, activity_stats._dac_5_code())) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['1', '2']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_3_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + + ''']) +def test_humanitarian_sector_true_3_digit(major_version, sector, xml): + """ + Detects an activity to be humanitarian using 3-digit sector codes considered to relate to humanitarian. + + Also checks that the appropriate vocabulary is provided or assumed. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, activity_stats._dac_3_code())) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.01', '2.02']) +@pytest.mark.parametrize('hum_sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('not_hum_sector', [-89, 'not_a_code', HUMANITARIAN_SECTOR_CODES_5_DIGITS[0]+1, HUMANITARIAN_SECTOR_CODES_3_DIGITS[0]+1, HUMANITARIAN_SECTOR_CODES_5_DIGITS[-1]-1, HUMANITARIAN_SECTOR_CODES_3_DIGITS[-1]-1]) +@pytest.mark.parametrize('xml', [''' + + + + + + + ''', ''' + + + + + + + ''', ''' + + + + + + + + ''', ''' + + + + + + + + ''', ''' + + + + + + + + ''', ''' + + + + + + ''']) +def test_humanitarian_sector_true_transaction(version, hum_sector, not_hum_sector, xml): + """ + Detects an activity to be humanitarian using sector codes at transaction level considered to relate to humanitarian. + + Also checks that the appropriate vocabulary is provided or assumed. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_sector, activity_stats._dac_5_code(), not_hum_sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', 'unknown version']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('xml', [''' + + + + + + + ''', ''' + + + + + + + ''']) +def test_humanitarian_sector_true_transaction_invalid_version(version, sector, xml): + """ + Detects an activity to be not humanitarian due to an invalid version despite correctly formed transactions. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(sector, activity_stats._dac_5_code())) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['1', '2']) +@pytest.mark.parametrize('sector', [-89, 'not_a_code', HUMANITARIAN_SECTOR_CODES_5_DIGITS[0]+1, HUMANITARIAN_SECTOR_CODES_3_DIGITS[0]+1, HUMANITARIAN_SECTOR_CODES_5_DIGITS[-1]-1, HUMANITARIAN_SECTOR_CODES_3_DIGITS[-1]-1]) +@pytest.mark.parametrize('xml', [''' + + + + ''']) +def test_humanitarian_sector_false_bad_codes(major_version, sector, xml): + """ + Detects an activity not to be humanitarian using sector codes that are not considered to relate to humanitarian. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['1', '2']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('vocab', [2, 99, 'DAC-3']) +@pytest.mark.parametrize('xml', [''' + + + + ''']) +def test_humanitarian_sector_false_bad_vocab(major_version, sector, vocab, xml): + """ + Detects an activity not to be humanitarian due to specification of an incorrect vocabulary despite @code values that are considered to relate to humanitarian by default. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, vocab)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['1', '2']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_3_DIGITS) +@pytest.mark.parametrize('vocab', [1, 99, 'DAC']) +@pytest.mark.parametrize('xml', [''' + + + + ''']) +def test_humanitarian_sector_false_bad_vocab_3_digit(major_version, sector, vocab, xml): + """ + Detects an activity not to be humanitarian due to specification of an incorrect vocabulary despite 3-digit @code values that are considered to relate to humanitarian by default. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, vocab)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_3_DIGITS + HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('vocab', ['1', '2', '']) +@pytest.mark.parametrize('xml', [''' + + + + ''']) +def test_humanitarian_sector_false_bad_major_version_1(sector, vocab, xml, major_version='1'): + """ + Detects an activity not to be humanitarian due to specification of a vocabulary that is valid at an alternative major version of the Standard. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, vocab)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_3_DIGITS + HUMANITARIAN_SECTOR_CODES_5_DIGITS) +@pytest.mark.parametrize('vocab', ['DAC', 'DAC-3', '']) +@pytest.mark.parametrize('xml', [''' + + + + ''', ''' + + + + + + + ''']) +def test_humanitarian_sector_false_bad_major_version_2(sector, vocab, xml, major_version='2'): + """ + Detects an activity not to be humanitarian due to specification of a vocabulary that is valid at an alternative major version of the Standard. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(xml.format(sector, vocab)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('major_version', ['2']) +@pytest.mark.parametrize('sector', HUMANITARIAN_SECTOR_CODES_5_DIGITS + [89, 'not_a_code']) +@pytest.mark.parametrize('hum_attrib_val', ['1', 'true']) +def test_humanitarian_attrib_true_sector_anything(major_version, sector, hum_attrib_val): + """ + Detect an activity to be humanitarian using @humanitarian values that evaluate to true in combination with sector codes. + + Both valid and invalid sector codes are tested because everything should still evaluate the same, given that @humanitarian is true. + """ + activity_stats = MockActivityStats(major_version) + + activity_stats.element = etree.fromstring(''' + + + + '''.format(hum_attrib_val, sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 1 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('sector', [-89, 'not_a_code']) +@pytest.mark.parametrize('hum_attrib_val', ['0', 'false', 'True', 'False', '']) +def test_humanitarian_attrib_false_sector_false(version, sector, hum_attrib_val): + """ + Detect an activity not to be humanitarian using sector codes that are deemed not to be humanitarian, in combination with a @humanitarian value which evaluates to false. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + '''.format(hum_attrib_val, sector)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', 'unknown version']) +@pytest.mark.parametrize('hum_attrib_val_true', ['1', 'true']) +@pytest.mark.parametrize('hum_attrib_val_false', ['0', 'false', 'True', 'False', '']) +@pytest.mark.parametrize('xml', [''' + + + ''', ''' + + + + ''', ''' + + + + ''', ''' + + + + ''']) +def test_humanitarian_attrib_false_sector_false(version, hum_attrib_val_true, hum_attrib_val_false, xml): + """ + Detect an activity not to be humanitarian at versions of the standard not expecting @humanitarian when a @humanitarian value that evaluates to true is present. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(xml.format(hum_attrib_val_true, hum_attrib_val_false)) + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +@pytest.mark.parametrize('hum_attrib_val', ['1', 'true']) +def test_humanitarian_elements_valid_version(version, hum_attrib_val): + """ + Tests that humanitarian elements are detected at supported versions. + """ + + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + '''.format(hum_attrib_val)) + + assert activity_stats.humanitarian()['is_humanitarian'] == 1 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 1 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 1 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', 'unknown version']) +@pytest.mark.parametrize('hum_attrib_val', ['1', 'true']) +def test_humanitarian_elements_invalid_version(version, hum_attrib_val): + """ + Tests that humanitarian elements are detected at supported versions. + """ + + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + '''.format(version, hum_attrib_val)) + + assert activity_stats.humanitarian()['is_humanitarian'] == 0 + assert activity_stats.humanitarian()['is_humanitarian_by_attrib'] == 0 + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +def test_humanitarian_scope_valid(version): + """ + Detect that an activity contains a humanitarian-scope element and required attributes. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + ''') + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 1 + + +@pytest.mark.parametrize('version', ['2.02']) +def test_humanitarian_scope_invalid(version): + """ + Detect that an activity contains a humanitarian-scope element without required attributes. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + ''') + assert activity_stats.humanitarian()['contains_humanitarian_scope'] == 0 + + +@pytest.mark.parametrize('version', ['2.02']) +def test_humanitarian_clusters_valid(version): + """ + Detect that an activity contains a sector defined by the 'Humanitarian Global Clusters' sector vocabulary. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + ''') + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 1 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', 'unknown version']) +def test_humanitarian_clusters_version_1(version): + """ + Detect that a pre-2.02 activity containing a sector defined by the 'Humanitarian Global Clusters' sector vocabulary is not detected. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + ''') + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + + +@pytest.mark.parametrize('version', ['1.01', '1.02', '1.03', '1.04', '1.05', '2.01', '2.02', 'unknown version']) +@pytest.mark.parametrize('sector_vocabulary_code', ['', '1', 'internal vocabulary']) +def test_humanitarian_clusters_invalid(version, sector_vocabulary_code): + """ + Detect that an activity does not contain a sector defined by the 'Humanitarian Global Clusters' sector vocabulary. + """ + activity_stats = MockActivityStats(version) + + activity_stats.element = etree.fromstring(''' + + + + '''.format(sector_vocabulary_code)) + assert activity_stats.humanitarian()['uses_humanitarian_clusters_vocab'] == 0 + diff --git a/statsrunner/test_common.py b/statsrunner/test_common.py index 1225fdd84ef..e669bcac23d 100644 --- a/statsrunner/test_common.py +++ b/statsrunner/test_common.py @@ -1,7 +1,9 @@ from decimal import Decimal from statsrunner.common import decimal_default import json +import pytest +@pytest.mark.xfail(raises=AssertionError) def test_decimal_default(): assert json.dumps(Decimal('1.1'), default=decimal_default) == '1.1' assert json.dumps(Decimal('42'), default=decimal_default) == '42'