Path: blob/master/tests/compatibility_test/run_compatibility_test.py
20987 views
#!/usr/bin/env python31from __future__ import annotations23import itertools4import json5import os6import pathlib7import subprocess8import urllib.request9from typing import Any1011PROJECT_PATH = pathlib.Path(__file__).parent.resolve().joinpath("godot")12CLASS_METHODS_FILE = PROJECT_PATH.joinpath("class_methods.txt")13BUILTIN_METHODS_FILE = PROJECT_PATH.joinpath("builtin_methods.txt")14UTILITY_FUNCTIONS_FILE = PROJECT_PATH.joinpath("utility_functions.txt")151617def download_gdextension_api(reftag: str) -> dict[str, Any]:18with urllib.request.urlopen(19f"https://raw.githubusercontent.com/godotengine/godot-cpp/godot-{reftag}/gdextension/extension_api.json"20) as f:21gdextension_api_json: dict[str, Any] = json.load(f)22return gdextension_api_json232425def remove_test_data_files():26for test_data in [CLASS_METHODS_FILE, BUILTIN_METHODS_FILE, UTILITY_FUNCTIONS_FILE]:27if os.path.isfile(test_data):28os.remove(test_data)293031def generate_test_data_files(reftag: str):32"""33Parses methods specified in given Godot version into a form readable by the compatibility checker GDExtension.34"""35gdextension_reference_json = download_gdextension_api(reftag)3637with open(CLASS_METHODS_FILE, "w") as classes_file:38classes_file.writelines(39[40f"{klass['name']} {func['name']} {func['hash']}\n"41for (klass, func) in itertools.chain(42(43(klass, method)44for klass in gdextension_reference_json["classes"]45for method in klass.get("methods", [])46if not method.get("is_virtual")47),48)49]50)5152variant_types: dict[str, int] | None = None53for global_enum in gdextension_reference_json["global_enums"]:54if global_enum.get("name") != "Variant.Type":55continue56variant_types = {57variant_type.get("name").removeprefix("TYPE_").lower().replace("_", ""): variant_type.get("value")58for variant_type in global_enum.get("values")59}6061if not variant_types:62return6364with open(BUILTIN_METHODS_FILE, "w") as f:65f.writelines(66[67f"{variant_types[klass['name'].lower()]} {func['name']} {func['hash']}\n"68for (klass, func) in itertools.chain(69(70(klass, method)71for klass in gdextension_reference_json["builtin_classes"]72for method in klass.get("methods", [])73),74)75]76)7778with open(UTILITY_FUNCTIONS_FILE, "w") as f:79f.writelines([f"{func['name']} {func['hash']}\n" for func in gdextension_reference_json["utility_functions"]])808182def has_compatibility_test_failed(errors: str) -> bool:83"""84Checks if provided errors are related to the compatibility test.8586Makes sure that test won't fail on unrelated account (for example editor misconfiguration).87"""88compatibility_errors = [89"Error loading extension",90"Failed to load interface method",91'Parameter "mb" is null.',92'Parameter "bfi" is null.',93"Method bind not found:",94"Utility function not found:",95"has changed and no compatibility fallback has been provided",96"Failed to open file `builtin_methods.txt`",97"Failed to open file `class_methods.txt`",98"Failed to open file `utility_functions.txt`",99"Failed to open file `platform_methods.txt`",100"Outcome = FAILURE",101]102103return any(compatibility_error in errors for compatibility_error in compatibility_errors)104105106def process_compatibility_test(proc: subprocess.Popen[bytes], timeout: int = 5) -> str | None:107"""108Returns the stderr output as a string, if any.109110Terminates test if nothing has been written to stdout/stderr for specified time.111"""112errors = bytearray()113114while True:115try:116_out, err = proc.communicate(timeout=timeout)117if err:118errors.extend(err)119except subprocess.TimeoutExpired:120proc.kill()121_out, err = proc.communicate()122if err:123errors.extend(err)124break125126return errors.decode("utf-8") if errors else None127128129def compatibility_check(godot4_bin: str) -> bool:130"""131Checks if methods specified for previous Godot versions can be properly loaded with132the latest Godot4 binary.133"""134# A bit crude albeit working solution – use stderr to check for compatibility-related errors.135proc = subprocess.Popen(136[godot4_bin, "--headless", "-e", "--path", PROJECT_PATH],137stdout=subprocess.PIPE,138stderr=subprocess.PIPE,139)140141if (errors := process_compatibility_test(proc)) and has_compatibility_test_failed(errors):142print(f"Compatibility test failed. Errors:\n {errors}")143return False144return True145146147if __name__ == "__main__":148godot4_bin = os.environ["GODOT4_BIN"]149reftags = os.environ["REFTAGS"].split(",")150is_success = True151for reftag in reftags:152generate_test_data_files(reftag)153if not compatibility_check(godot4_bin):154print(f"Compatibility test against Godot{reftag} failed")155is_success = False156remove_test_data_files()157158if not is_success:159exit(1)160161162