godot-module-template/engine/tests/compatibility_test/run_compatibility_test.py
Sara c3f9669b10 Add 'engine/' from commit 'a8e37fc010'
git-subtree-dir: engine
git-subtree-mainline: b74841629e
git-subtree-split: a8e37fc010
2026-03-13 11:22:19 +01:00

156 lines
5.2 KiB
Python
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
from __future__ import annotations
import itertools
import json
import os
import pathlib
import subprocess
import urllib.request
from typing import Any
PROJECT_PATH = pathlib.Path(__file__).parent.resolve().joinpath("godot")
CLASS_METHODS_FILE = PROJECT_PATH.joinpath("class_methods.txt")
BUILTIN_METHODS_FILE = PROJECT_PATH.joinpath("builtin_methods.txt")
UTILITY_FUNCTIONS_FILE = PROJECT_PATH.joinpath("utility_functions.txt")
def download_gdextension_api(reftag: str) -> dict[str, Any]:
with urllib.request.urlopen(
f"https://raw.githubusercontent.com/godotengine/godot-cpp/godot-{reftag}/gdextension/extension_api.json"
) as f:
gdextension_api_json: dict[str, Any] = json.load(f)
return gdextension_api_json
def remove_test_data_files():
for test_data in [CLASS_METHODS_FILE, BUILTIN_METHODS_FILE, UTILITY_FUNCTIONS_FILE]:
if os.path.isfile(test_data):
os.remove(test_data)
def generate_test_data_files(reftag: str):
"""
Parses methods specified in given Godot version into a form readable by the compatibility checker GDExtension.
"""
gdextension_reference_json = download_gdextension_api(reftag)
with open(CLASS_METHODS_FILE, "w") as classes_file:
classes_file.writelines([
f"{klass['name']} {func['name']} {func['hash']}\n"
for (klass, func) in itertools.chain(
(
(klass, method)
for klass in gdextension_reference_json["classes"]
for method in klass.get("methods", [])
if not method.get("is_virtual")
),
)
])
variant_types: dict[str, int] | None = None
for global_enum in gdextension_reference_json["global_enums"]:
if global_enum.get("name") != "Variant.Type":
continue
variant_types = {
variant_type.get("name").removeprefix("TYPE_").lower().replace("_", ""): variant_type.get("value")
for variant_type in global_enum.get("values")
}
if not variant_types:
return
with open(BUILTIN_METHODS_FILE, "w") as f:
f.writelines([
f"{variant_types[klass['name'].lower()]} {func['name']} {func['hash']}\n"
for (klass, func) in itertools.chain(
(
(klass, method)
for klass in gdextension_reference_json["builtin_classes"]
for method in klass.get("methods", [])
),
)
])
with open(UTILITY_FUNCTIONS_FILE, "w") as f:
f.writelines([f"{func['name']} {func['hash']}\n" for func in gdextension_reference_json["utility_functions"]])
def has_compatibility_test_failed(errors: str) -> bool:
"""
Checks if provided errors are related to the compatibility test.
Makes sure that test won't fail on unrelated account (for example editor misconfiguration).
"""
compatibility_errors = [
"Error loading extension",
"Failed to load interface method",
'Parameter "mb" is null.',
'Parameter "bfi" is null.',
"Method bind not found:",
"Utility function not found:",
"has changed and no compatibility fallback has been provided",
"Failed to open file `builtin_methods.txt`",
"Failed to open file `class_methods.txt`",
"Failed to open file `utility_functions.txt`",
"Failed to open file `platform_methods.txt`",
"Outcome = FAILURE",
]
return any(compatibility_error in errors for compatibility_error in compatibility_errors)
def process_compatibility_test(proc: subprocess.Popen[bytes], timeout: int = 5) -> str | None:
"""
Returns the stderr output as a string, if any.
Terminates test if nothing has been written to stdout/stderr for specified time.
"""
errors = bytearray()
while True:
try:
_out, err = proc.communicate(timeout=timeout)
if err:
errors.extend(err)
except subprocess.TimeoutExpired:
proc.kill()
_out, err = proc.communicate()
if err:
errors.extend(err)
break
return errors.decode("utf-8") if errors else None
def compatibility_check(godot4_bin: str) -> bool:
"""
Checks if methods specified for previous Godot versions can be properly loaded with
the latest Godot4 binary.
"""
# A bit crude albeit working solution use stderr to check for compatibility-related errors.
proc = subprocess.Popen(
[godot4_bin, "--headless", "-e", "--path", PROJECT_PATH],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
if (errors := process_compatibility_test(proc)) and has_compatibility_test_failed(errors):
print(f"Compatibility test failed. Errors:\n {errors}")
return False
return True
if __name__ == "__main__":
godot4_bin = os.environ["GODOT4_BIN"]
reftags = os.environ["REFTAGS"].split(",")
is_success = True
for reftag in reftags:
generate_test_data_files(reftag)
if not compatibility_check(godot4_bin):
print(f"Compatibility test against Godot{reftag} failed")
is_success = False
remove_test_data_files()
if not is_success:
exit(1)