File size: 8,959 Bytes
0f2ecac
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
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