Skip to content

Commit f7cf52e

Browse files
committed
Update test
1 parent b056fe2 commit f7cf52e

5 files changed

Lines changed: 161 additions & 114 deletions

File tree

.github/workflows/validate-profile-and-schemas.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ name: Validate profile and schemas
22

33
on:
44
push:
5-
branches: [main]
65
pull_request:
7-
branches: [main]
86

97
jobs:
108
validate-profile-and-schemas:

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ Create a production-style build:
3535
```bash
3636
JEKYLL_ENV=production bundle exec jekyll build
3737
```
38+
39+
Run validation tests:
40+
41+
```bash
42+
python3 -m venv .venv
43+
source .venv/bin/activate
44+
python -m pip install -r tests/requirements.txt
45+
python tests/validate_profile_and_schemas.py
46+
```

tests/helpers.py

Lines changed: 0 additions & 57 deletions
This file was deleted.

tests/requirements.txt

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,2 @@
1-
attrs==23.1.0
2-
black==21.9b0
3-
certifi==2021.10.8
4-
chardet==4.0.0
5-
charset-normalizer==2.0.7
6-
click==8.0.3
7-
colorama==0.4.4
8-
decorator==5.1.0
1+
# Minimal dependencies for tests/validate_profile_and_schemas.py
92
frictionless>=5.13.1
10-
humanize==4.6.0
11-
idna==3.3
12-
isodate==0.6.0
13-
Jinja2==3.1.2
14-
jsonschema>=4.20
15-
markdown-it-py==2.2.0
16-
marko==1.1.0
17-
MarkupSafe==2.1.2
18-
mdurl==0.1.2
19-
mypy==0.910
20-
mypy-extensions==0.4.3
21-
pathspec==0.9.0
22-
petl==1.7.4
23-
platformdirs==2.4.0
24-
pydantic>=2.0
25-
Pygments==2.15.1
26-
pyrsistent==0.18.0
27-
python-dateutil==2.8.2
28-
python-slugify==5.0.2
29-
PyYAML==6.0
30-
regex==2021.10.23
31-
reportbro-simpleeval==0.9.11
32-
requests==2.26.0
33-
rfc3986==1.5.0
34-
rich==13.3.5
35-
shellingham==1.4.0
36-
simpleeval==0.9.11
37-
six==1.16.0
38-
stringcase==1.2.0
39-
tabulate==0.9.0
40-
text-unidecode==1.3
41-
toml==0.10.2
42-
tomli==1.2.1
43-
typer>=0.12
44-
typing_extensions==4.6.1
45-
urllib3==1.26.7
46-
validators==0.18.2

tests/validate_profile_and_schemas.py

Lines changed: 151 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,40 @@
33
import sys
44
import json
55
from pathlib import Path
6-
from typing import List
6+
from typing import Dict, List, Optional
77
from frictionless import Schema
8-
from helpers import PROFILE_PATH, TABLE_SCHEMA_PATHS
98

9+
THIS_SCRIPT_PATH = Path(__file__).parent
10+
REPOSITORY_ROOT_PATH = THIS_SCRIPT_PATH / ".."
11+
PROFILE_PATH = REPOSITORY_ROOT_PATH / "geolocator-dp-profile.json"
12+
TABLE_SCHEMA_PATHS = [
13+
REPOSITORY_ROOT_PATH / "observations-table-schema.json",
14+
REPOSITORY_ROOT_PATH / "tags-table-schema.json",
15+
REPOSITORY_ROOT_PATH / "measurements-table-schema.json",
16+
REPOSITORY_ROOT_PATH / "staps-table-schema.json",
17+
REPOSITORY_ROOT_PATH / "twilights-table-schema.json",
18+
REPOSITORY_ROOT_PATH / "paths-table-schema.json",
19+
REPOSITORY_ROOT_PATH / "edges-table-schema.json",
20+
REPOSITORY_ROOT_PATH / "pressurepaths-table-schema.json",
21+
]
1022

11-
def validate_json(filepath: Path) -> bool:
23+
EXPECTED_SCHEMA_RESOURCES = {
24+
"tags",
25+
"observations",
26+
"measurements",
27+
"staps",
28+
"twilights",
29+
"paths",
30+
"edges",
31+
"pressurepaths",
32+
}
33+
34+
def load_json(filepath: Path) -> Optional[dict]:
1235
with open(filepath) as file:
1336
try:
14-
json.load(file)
37+
return json.load(file)
1538
except json.decoder.JSONDecodeError:
16-
return False
17-
else:
18-
return True
39+
return None
1940

2041

2142
def validate_schema(file_path: Path) -> bool:
@@ -32,12 +53,124 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
3253
return [err.message for err in report.errors]
3354

3455

56+
def _listify_fields(fields) -> List[str]:
57+
if isinstance(fields, str):
58+
return [fields]
59+
if isinstance(fields, list):
60+
return [field for field in fields if isinstance(field, str)]
61+
return []
62+
63+
64+
def _get_schema_fields(descriptor: dict) -> set:
65+
fields = descriptor.get("fields", [])
66+
return {
67+
field.get("name")
68+
for field in fields
69+
if isinstance(field, dict) and isinstance(field.get("name"), str)
70+
}
71+
72+
73+
def check_schema_coherence(schema_descriptors: Dict[Path, dict]) -> bool:
74+
encountered_errors = False
75+
resource_to_paths: Dict[str, List[Path]] = {}
76+
77+
# 1) filename <-> schema name consistency
78+
for schema_path, descriptor in schema_descriptors.items():
79+
schema_name = descriptor.get("name")
80+
expected_name = schema_path.name.replace("-table-schema.json", "")
81+
82+
if not isinstance(schema_name, str):
83+
print(f"✕ {schema_path.name}: missing or non-string `name`")
84+
encountered_errors = True
85+
continue
86+
87+
resource_to_paths.setdefault(schema_name, []).append(schema_path)
88+
89+
if schema_name != expected_name:
90+
print(
91+
f"✕ {schema_path.name}: schema `name` is `{schema_name}` but expected `{expected_name}`"
92+
)
93+
encountered_errors = True
94+
95+
# 2) expected resources exist exactly once
96+
observed_resources = set(resource_to_paths.keys())
97+
missing_resources = sorted(EXPECTED_SCHEMA_RESOURCES - observed_resources)
98+
unexpected_resources = sorted(observed_resources - EXPECTED_SCHEMA_RESOURCES)
99+
100+
for resource in missing_resources:
101+
print(f"✕ schema resources: missing expected resource `{resource}`")
102+
encountered_errors = True
103+
104+
for resource in unexpected_resources:
105+
print(f"✕ schema resources: unexpected resource `{resource}`")
106+
encountered_errors = True
107+
108+
for resource, paths in sorted(resource_to_paths.items()):
109+
if len(paths) > 1:
110+
path_list = ", ".join(path.name for path in paths)
111+
print(
112+
f"✕ schema resources: resource `{resource}` is defined more than once ({path_list})"
113+
)
114+
encountered_errors = True
115+
116+
# 3) foreign keys reference existing resources and target fields
117+
fields_by_resource: Dict[str, set] = {}
118+
for resource, paths in resource_to_paths.items():
119+
if len(paths) == 1:
120+
fields_by_resource[resource] = _get_schema_fields(schema_descriptors[paths[0]])
121+
122+
for schema_path, descriptor in schema_descriptors.items():
123+
foreign_keys = descriptor.get("foreignKeys", [])
124+
if not isinstance(foreign_keys, list):
125+
continue
126+
127+
for foreign_key in foreign_keys:
128+
if not isinstance(foreign_key, dict):
129+
continue
130+
131+
reference = foreign_key.get("reference", {})
132+
if not isinstance(reference, dict):
133+
print(f"✕ {schema_path.name}: malformed foreign key reference")
134+
encountered_errors = True
135+
continue
136+
137+
target_resource = reference.get("resource")
138+
target_fields = _listify_fields(reference.get("fields"))
139+
140+
if not isinstance(target_resource, str):
141+
print(f"✕ {schema_path.name}: foreign key reference missing `resource`")
142+
encountered_errors = True
143+
continue
144+
145+
if target_resource not in resource_to_paths:
146+
print(
147+
f"✕ {schema_path.name}: foreign key references unknown resource `{target_resource}`"
148+
)
149+
encountered_errors = True
150+
continue
151+
152+
target_resource_fields = fields_by_resource.get(target_resource, set())
153+
missing_target_fields = [
154+
field for field in target_fields if field not in target_resource_fields
155+
]
156+
157+
if missing_target_fields:
158+
fields_str = ", ".join(missing_target_fields)
159+
print(
160+
f"✕ {schema_path.name}: foreign key references missing field(s) in `{target_resource}`: {fields_str}"
161+
)
162+
encountered_errors = True
163+
164+
return not encountered_errors
165+
166+
35167
if __name__ == "__main__":
36168
encountered_errors = False
169+
schema_descriptors: Dict[Path, dict] = {}
37170

38171
print(PROFILE_PATH.name)
39-
result = validate_json(PROFILE_PATH)
40-
if result is True:
172+
profile_json = load_json(PROFILE_PATH)
173+
if profile_json is not None:
41174
print("✔︎ valid JSON")
42175
else:
43176
print("✕ valid JSON")
@@ -46,7 +179,9 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
46179
for table_schema in TABLE_SCHEMA_PATHS:
47180
print(f"\n{table_schema.name}")
48181

49-
if validate_json(table_schema):
182+
schema_json = load_json(table_schema)
183+
if schema_json is not None:
184+
schema_descriptors[table_schema] = schema_json
50185
print("✔︎ valid JSON")
51186
if validate_schema(table_schema):
52187
print("✔︎ valid Table Schema")
@@ -59,6 +194,12 @@ def get_schema_metadata_error_messages(file_path: Path) -> List[str]:
59194
print("✕ valid JSON")
60195
encountered_errors = True
61196

197+
print("\nSchema coherence")
198+
if check_schema_coherence(schema_descriptors):
199+
print("✔︎ schema coherence checks passed")
200+
else:
201+
encountered_errors = True
202+
62203
if encountered_errors:
63204
print("Errors were encountered")
64205
sys.exit(1)

0 commit comments

Comments
 (0)