forked from zulip/zulip
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcheck-schemas
executable file
·239 lines (189 loc) · 6.72 KB
/
check-schemas
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
#!/usr/bin/env python3
#
# Validates that 3 data sources agree about the structure of Zulip's events API:
#
# * Node fixtures for the server_events_dispatch.js tests.
# * OpenAPI definitions in zerver/openapi/zulip.yaml
# * The schemas defined in zerver/lib/events_schema.py used for the
# Zulip server's test suite.
#
# We compare the Python and OpenAPI schemas by converting the OpenAPI data
# into the event_schema style of types and the diffing the schemas.
import argparse
import difflib
import os
import subprocess
import sys
from typing import Any, Callable, Dict, List, Optional
import orjson
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(TOOLS_DIR))
ROOT_DIR = os.path.dirname(TOOLS_DIR)
EVENTS_JS = "web/tests/lib/events.js"
# check for the venv
from tools.lib import sanity_check
sanity_check.check_venv(__file__)
USAGE = """
This program reads in fixture data for our
node tests, and then it validates the fixture
data with checkers from event_schema.py (which
are the same Python functions we use to validate
events in test_events.py).
It currently takes no arguments.
"""
parser = argparse.ArgumentParser(usage=USAGE)
parser.parse_args()
# We can eliminate the django dependency in event_schema,
# but unfortunately it"s coupled to modules like validate.py
# and topic.py.
import django
os.environ["DJANGO_SETTINGS_MODULE"] = "zproject.test_settings"
django.setup()
from zerver.lib import event_schema
from zerver.lib.data_types import (
DictType,
EnumType,
ListType,
NumberType,
StringDictType,
UnionType,
make_checker,
schema,
)
from zerver.openapi.openapi import openapi_spec
# This list of exemptions represents details we should fix in Zulip's
# API structure and/or validators.
EXEMPT_OPENAPI_NAMES = [
# users field missing
"update_display_settings_event",
"update_global_notifications_event",
# Additional keys(push_users_notify) due to bug in API.
"message_event",
# tuple handling
"muted_topics_event",
# bots, delivery_email, profile_data
"realm_user_add_event",
# OpenAPI is incomplete
"realm_update_dict_event",
# is_mirror_dummy
"reaction_add_event",
"reaction_remove_event",
]
# This is a list of events still documented in the OpenAPI that
# are deprecated and no longer checked in event_schema.py.
DEPRECATED_EVENTS = [
"realm_filters_event",
]
def get_event_checker(event: Dict[str, Any]) -> Optional[Callable[[str, Dict[str, Any]], None]]:
name = event["type"]
if "op" in event:
name += "_" + event["op"]
name += "_event"
if hasattr(event_schema, name):
return make_checker(getattr(event_schema, name))
return None
def check_event(name: str, event: Dict[str, Any]) -> None:
event["id"] = 1
checker = get_event_checker(event)
if checker is not None:
try:
checker(name, event)
except AssertionError:
print(f"\n{EVENTS_JS} has bad data for {name}:\n\n")
raise
else:
print(f"WARNING - NEED SCHEMA: {name}")
def read_fixtures() -> Dict[str, Any]:
cmd = [
"node",
os.path.join(TOOLS_DIR, "node_lib/dump_fixtures.js"),
]
schema = subprocess.check_output(cmd)
return orjson.loads(schema)
def verify_fixtures_are_sorted(names: List[str]) -> None:
for i in range(1, len(names)):
if names[i] < names[i - 1]:
raise Exception(
f"""
Please keep your fixtures in order within
your events.js file. The following
key is out of order
{names[i]}
"""
)
def from_openapi(node: Dict[str, Any]) -> Any:
"""Converts the OpenAPI data into event_schema.py style type
definitions for convenient comparison with the types used for backend
tests declared there."""
if "oneOf" in node:
return UnionType([from_openapi(n) for n in node["oneOf"]])
if node["type"] == "object":
if (
"additionalProperties" in node
# this might be a glitch in our current spec? or
# maybe I just understand it yet
and isinstance(node["additionalProperties"], dict)
):
return StringDictType(from_openapi(node["additionalProperties"]))
if "properties" not in node:
return dict
required_keys = []
for key, sub_node in node["properties"].items():
required_keys.append((key, from_openapi(sub_node)))
return DictType(required_keys)
if node["type"] == "boolean":
return bool
if node["type"] == "integer":
if "enum" in node:
return EnumType(node["enum"])
return int
if node["type"] == "number":
return NumberType()
if node["type"] == "string":
if "enum" in node:
return EnumType(node["enum"])
return str
if node["type"] == "array":
return ListType(from_openapi(node["items"]))
raise AssertionError("cannot handle node")
def validate_openapi_against_event_schema() -> None:
node = openapi_spec.openapi()["paths"]["/events"]["get"]["responses"]["200"]["content"][
"application/json"
]["schema"]["properties"]["events"]["items"]["oneOf"]
for sub_node in node:
name = sub_node["properties"]["type"]["enum"][0]
if "op" in sub_node["properties"]:
name += "_" + sub_node["properties"]["op"]["enum"][0]
name += "_event"
if not hasattr(event_schema, name):
if name not in DEPRECATED_EVENTS:
print("WARNING - NEED SCHEMA to match OpenAPI", name)
continue
openapi_type = from_openapi(sub_node)
openapi_schema = schema(name, openapi_type)
py_type = getattr(event_schema, name)
py_schema = schema(name, py_type)
if name in EXEMPT_OPENAPI_NAMES:
if openapi_schema == py_schema:
raise AssertionError(f"unnecessary exemption for {name}")
continue
if openapi_schema != py_schema:
print(f"py\n{py_schema}\n")
print(f"openapi\n{openapi_schema}\n")
for line in difflib.unified_diff(
py_schema.split("\n"),
openapi_schema.split("\n"),
fromfile="py",
tofile="openapi",
):
print(line)
raise AssertionError("openapi schemas disagree")
def run() -> None:
fixtures = read_fixtures()
verify_fixtures_are_sorted(list(fixtures.keys()))
for name, event in fixtures.items():
check_event(name, event)
validate_openapi_against_event_schema()
print("Successful check. All tests passed.")
if __name__ == "__main__":
run()