| from __future__ import annotations |
|
|
| import copy |
| import json |
| from pathlib import Path |
|
|
| import pytest |
|
|
| from gcmd_classifier.errors import ( |
| DuplicateCanonicalPathError, |
| DuplicateUUIDError, |
| InvalidHierarchyTransitionError, |
| MalformedHierarchyError, |
| VocabularyLookupError, |
| ) |
| from gcmd_classifier.vocabulary import build_vocabulary_index, calculate_file_hash, load_vocabulary |
|
|
| FIXTURE_PATH = Path("tests/fixtures/gcmd_hierarchy_small.json") |
| FULL_HIERARCHY_PATH = Path("data/gcmd_hierarchy.json") |
|
|
|
|
| def load_fixture_data() -> dict: |
| return json.loads(FIXTURE_PATH.read_text()) |
|
|
|
|
| def build_fixture_index(): |
| return load_vocabulary(FIXTURE_PATH) |
|
|
|
|
| def test_recursive_traversal_and_variable_depth_branches() -> None: |
| index = build_fixture_index() |
|
|
| assert len(index) == 10 |
| assert index.root_level == "Category" |
| assert index.root_name == "EARTH SCIENCE" |
| assert index.children_of(None) == ("topic-atmosphere", "topic-oceans") |
| assert index.children_of("term-weather-events") == () |
| assert index.get("vl3-carbon-dioxide-profiles").level == "Variable_Level_3" |
|
|
|
|
| def test_uuid_preservation_and_record_fields() -> None: |
| index = build_fixture_index() |
| record = index.get("vl3-carbon-dioxide-profiles") |
|
|
| assert record.UUID == "vl3-carbon-dioxide-profiles" |
| assert record.name == "CARBON DIOXIDE PROFILES" |
| assert record.level == "Variable_Level_3" |
| assert record.topic == "ATMOSPHERE" |
| assert record.term == "ATMOSPHERIC CHEMISTRY" |
| assert record.path_components == ( |
| "ATMOSPHERE", |
| "ATMOSPHERIC CHEMISTRY", |
| "CARBON", |
| "CARBON DIOXIDE", |
| "CARBON DIOXIDE PROFILES", |
| ) |
| assert ( |
| record.canonical_path == "ATMOSPHERE > ATMOSPHERIC CHEMISTRY > CARBON > CARBON DIOXIDE > " |
| "CARBON DIOXIDE PROFILES" |
| ) |
| assert record.parent_uuid == "vl2-carbon-dioxide" |
| assert record.parent_name == "CARBON DIOXIDE" |
| assert record.child_uuids == () |
| assert record.has_children is False |
| assert record.assignable is True |
| assert record.definition == "Vertical carbon dioxide profiles." |
| assert record.vocabulary_version == calculate_file_hash(FIXTURE_PATH) |
|
|
|
|
| def test_topic_term_and_variable_records_are_assignable() -> None: |
| index = build_fixture_index() |
|
|
| topic = index.get("topic-atmosphere") |
| term = index.get("term-atmospheric-chemistry") |
| internal_variable = index.get("vl1-atmosphere-carbon") |
| leaf_variable = index.get("vl2-methane") |
|
|
| assert topic.level == "Topic" |
| assert term.level == "Term" |
| assert internal_variable.level == "Variable_Level_1" |
| assert leaf_variable.level == "Variable_Level_2" |
| assert topic.assignable is True |
| assert term.assignable is True |
| assert internal_variable.assignable is True |
| assert leaf_variable.assignable is True |
| assert internal_variable.has_children is True |
| assert leaf_variable.has_children is False |
|
|
|
|
| def test_root_category_is_non_assignable_context() -> None: |
| index = build_fixture_index() |
|
|
| assert "EARTH SCIENCE" not in index.records_by_path |
| assert all(record.level != "Category" for record in index.records_by_uuid.values()) |
| assert index.parent_of("topic-atmosphere") is None |
| assert index.get("topic-atmosphere").parent_name == "EARTH SCIENCE" |
|
|
|
|
| def test_optional_and_missing_definitions() -> None: |
| index = build_fixture_index() |
|
|
| assert index.get("topic-atmosphere").definition == "Atmospheric science." |
| assert index.get("vl2-methane").definition is None |
|
|
|
|
| def test_canonical_path_excludes_root_category() -> None: |
| index = build_fixture_index() |
|
|
| assert index.get("topic-atmosphere").canonical_path == "ATMOSPHERE" |
| assert index.get("term-weather-events").canonical_path == "ATMOSPHERE > WEATHER EVENTS" |
| assert not index.get("topic-atmosphere").canonical_path.startswith("EARTH SCIENCE") |
|
|
|
|
| def test_identical_names_in_different_branches_are_allowed_when_paths_differ() -> None: |
| index = build_fixture_index() |
|
|
| atmosphere_carbon = index.get("vl1-atmosphere-carbon") |
| ocean_carbon = index.get("vl1-ocean-carbon") |
|
|
| assert atmosphere_carbon.name == ocean_carbon.name == "CARBON" |
| assert atmosphere_carbon.canonical_path != ocean_carbon.canonical_path |
|
|
|
|
| def test_duplicate_uuid_detection() -> None: |
| data = load_fixture_data() |
| data["children"][1]["UUID"] = "topic-atmosphere" |
|
|
| with pytest.raises(DuplicateUUIDError): |
| build_vocabulary_index(data, vocabulary_version="test") |
|
|
|
|
| def test_duplicate_canonical_path_detection() -> None: |
| data = load_fixture_data() |
| duplicate = copy.deepcopy(data["children"][0]["children"][1]) |
| duplicate["UUID"] = "term-weather-events-duplicate" |
| data["children"][0]["children"].append(duplicate) |
|
|
| with pytest.raises(DuplicateCanonicalPathError): |
| build_vocabulary_index(data, vocabulary_version="test") |
|
|
|
|
| @pytest.mark.parametrize("field", ["level", "name"]) |
| def test_missing_required_fields(field: str) -> None: |
| data = load_fixture_data() |
| del data["children"][0][field] |
|
|
| with pytest.raises(MalformedHierarchyError): |
| build_vocabulary_index(data, vocabulary_version="test") |
|
|
|
|
| def test_unexpected_uuidless_non_root_node() -> None: |
| data = load_fixture_data() |
| del data["children"][0]["UUID"] |
|
|
| with pytest.raises(MalformedHierarchyError): |
| build_vocabulary_index(data, vocabulary_version="test") |
|
|
|
|
| def test_valid_hierarchy_level_transitions() -> None: |
| index = build_fixture_index() |
|
|
| assert index.get("topic-atmosphere").child_uuids == ( |
| "term-atmospheric-chemistry", |
| "term-weather-events", |
| ) |
| assert index.get("term-atmospheric-chemistry").child_uuids == ("vl1-atmosphere-carbon",) |
| assert index.get("vl1-atmosphere-carbon").child_uuids == ( |
| "vl2-carbon-dioxide", |
| "vl2-methane", |
| ) |
| assert index.get("vl2-carbon-dioxide").child_uuids == ("vl3-carbon-dioxide-profiles",) |
|
|
|
|
| def test_invalid_hierarchy_level_transition() -> None: |
| data = load_fixture_data() |
| data["children"][0]["children"][0]["level"] = "Variable_Level_1" |
|
|
| with pytest.raises(InvalidHierarchyTransitionError): |
| build_vocabulary_index(data, vocabulary_version="test") |
|
|
|
|
| def test_parent_and_direct_child_lookup() -> None: |
| index = build_fixture_index() |
|
|
| assert index.parent_of("vl2-carbon-dioxide") == "vl1-atmosphere-carbon" |
| assert index.children_of("vl1-atmosphere-carbon") == ( |
| "vl2-carbon-dioxide", |
| "vl2-methane", |
| ) |
| with pytest.raises(VocabularyLookupError): |
| index.parent_of("missing-uuid") |
|
|
|
|
| def test_topic_to_term_lookup() -> None: |
| index = build_fixture_index() |
|
|
| terms = index.terms_for_topic("topic-atmosphere") |
| assert tuple(term.UUID for term in terms) == ( |
| "term-atmospheric-chemistry", |
| "term-weather-events", |
| ) |
| with pytest.raises(VocabularyLookupError): |
| index.terms_for_topic("term-weather-events") |
|
|
|
|
| def test_term_to_variable_lookup() -> None: |
| index = build_fixture_index() |
|
|
| variables = index.variables_for_parent("term-atmospheric-chemistry") |
| assert tuple(variable.UUID for variable in variables) == ("vl1-atmosphere-carbon",) |
| assert index.variables_for_parent("vl2-methane") == () |
| with pytest.raises(VocabularyLookupError): |
| index.variables_for_parent("topic-atmosphere") |
|
|
|
|
| def test_ancestor_and_descendant_checks() -> None: |
| index = build_fixture_index() |
|
|
| assert index.is_ancestor("topic-atmosphere", "vl3-carbon-dioxide-profiles") is True |
| assert index.is_descendant("vl3-carbon-dioxide-profiles", "topic-atmosphere") is True |
| assert index.is_ancestor("term-weather-events", "vl3-carbon-dioxide-profiles") is False |
| assert index.ancestors_of("vl3-carbon-dioxide-profiles") == ( |
| "vl2-carbon-dioxide", |
| "vl1-atmosphere-carbon", |
| "term-atmospheric-chemistry", |
| "topic-atmosphere", |
| ) |
|
|
|
|
| def test_vocabulary_file_hash_stability() -> None: |
| assert calculate_file_hash(FIXTURE_PATH) == calculate_file_hash(FIXTURE_PATH) |
| assert build_fixture_index().vocabulary_version == calculate_file_hash(FIXTURE_PATH) |
|
|
|
|
| def test_source_file_immutability() -> None: |
| before = FULL_HIERARCHY_PATH.read_bytes() |
| load_vocabulary(FULL_HIERARCHY_PATH) |
| after = FULL_HIERARCHY_PATH.read_bytes() |
|
|
| assert after == before |
|
|
|
|
| def test_every_current_topic_and_term_has_uuid() -> None: |
| index = load_vocabulary(FULL_HIERARCHY_PATH) |
|
|
| assert all(topic.UUID for topic in index.topics()) |
| assert all(term.UUID for topic in index.topics() for term in index.terms_for_topic(topic.UUID)) |
|
|
|
|
| def test_full_data_smoke_counts_for_current_vocabulary() -> None: |
| index = load_vocabulary(FULL_HIERARCHY_PATH) |
| counts = index.count_by_level() |
|
|
| assert counts["Topic"] == 14 |
| assert counts["Term"] == 144 |
| assert counts["Variable_Level_1"] == 1368 |
| assert counts["Variable_Level_2"] == 1456 |
| assert counts["Variable_Level_3"] == 553 |
| assert len(index) == 3535 |
|
|