#!/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()