Files
zulip/tools/check-schemas
2025-10-08 17:05:51 -07:00

127 lines
3.4 KiB
Python
Executable File

#!/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 os
import subprocess
import sys
from collections.abc import Callable
from typing import Any
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.cjs"
# 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
make_checker = event_schema.__dict__["make_checker"]
def get_event_checker(event: dict[str, Any]) -> Callable[[str, dict[str, Any]], None]:
# Follow the naming convention to find the event checker.
# Start by grabbing the event type.
name = event["type"]
# Handle things like AttachmentRemoveEvent
if "op" in event:
name += "_" + event["op"].title()
# Change to CamelCase
name = name.replace("_", " ").title().replace(" ", "")
# Use EventModernPresence type to check "presence" events
if name == "Presence":
name = "Modern" + name
# And add the prefix.
name = "Event" + name
if not hasattr(event_schema, name):
raise ValueError(f"We could not find {name} in event_schemas.py")
return make_checker(getattr(event_schema, name))
def check_event(name: str, event: dict[str, Any]) -> None:
event["id"] = 1
checker = get_event_checker(event)
try:
checker(name, event)
except AssertionError:
print(f"\n{EVENTS_JS} has bad data for {name}:\n\n")
raise
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.cjs file. The following
key is out of order
{names[i]}
"""
)
def run() -> None:
fixtures = read_fixtures()
verify_fixtures_are_sorted(list(fixtures.keys()))
for name, event in fixtures.items():
check_event(name, event)
print(f"Successfully checked {len(fixtures)} fixtures. All tests passed.")
if __name__ == "__main__":
run()