Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/tests/compatibility_test/run_compatibility_test.py
20987 views
1
#!/usr/bin/env python3
2
from __future__ import annotations
3
4
import itertools
5
import json
6
import os
7
import pathlib
8
import subprocess
9
import urllib.request
10
from typing import Any
11
12
PROJECT_PATH = pathlib.Path(__file__).parent.resolve().joinpath("godot")
13
CLASS_METHODS_FILE = PROJECT_PATH.joinpath("class_methods.txt")
14
BUILTIN_METHODS_FILE = PROJECT_PATH.joinpath("builtin_methods.txt")
15
UTILITY_FUNCTIONS_FILE = PROJECT_PATH.joinpath("utility_functions.txt")
16
17
18
def download_gdextension_api(reftag: str) -> dict[str, Any]:
19
with urllib.request.urlopen(
20
f"https://raw.githubusercontent.com/godotengine/godot-cpp/godot-{reftag}/gdextension/extension_api.json"
21
) as f:
22
gdextension_api_json: dict[str, Any] = json.load(f)
23
return gdextension_api_json
24
25
26
def remove_test_data_files():
27
for test_data in [CLASS_METHODS_FILE, BUILTIN_METHODS_FILE, UTILITY_FUNCTIONS_FILE]:
28
if os.path.isfile(test_data):
29
os.remove(test_data)
30
31
32
def generate_test_data_files(reftag: str):
33
"""
34
Parses methods specified in given Godot version into a form readable by the compatibility checker GDExtension.
35
"""
36
gdextension_reference_json = download_gdextension_api(reftag)
37
38
with open(CLASS_METHODS_FILE, "w") as classes_file:
39
classes_file.writelines(
40
[
41
f"{klass['name']} {func['name']} {func['hash']}\n"
42
for (klass, func) in itertools.chain(
43
(
44
(klass, method)
45
for klass in gdextension_reference_json["classes"]
46
for method in klass.get("methods", [])
47
if not method.get("is_virtual")
48
),
49
)
50
]
51
)
52
53
variant_types: dict[str, int] | None = None
54
for global_enum in gdextension_reference_json["global_enums"]:
55
if global_enum.get("name") != "Variant.Type":
56
continue
57
variant_types = {
58
variant_type.get("name").removeprefix("TYPE_").lower().replace("_", ""): variant_type.get("value")
59
for variant_type in global_enum.get("values")
60
}
61
62
if not variant_types:
63
return
64
65
with open(BUILTIN_METHODS_FILE, "w") as f:
66
f.writelines(
67
[
68
f"{variant_types[klass['name'].lower()]} {func['name']} {func['hash']}\n"
69
for (klass, func) in itertools.chain(
70
(
71
(klass, method)
72
for klass in gdextension_reference_json["builtin_classes"]
73
for method in klass.get("methods", [])
74
),
75
)
76
]
77
)
78
79
with open(UTILITY_FUNCTIONS_FILE, "w") as f:
80
f.writelines([f"{func['name']} {func['hash']}\n" for func in gdextension_reference_json["utility_functions"]])
81
82
83
def has_compatibility_test_failed(errors: str) -> bool:
84
"""
85
Checks if provided errors are related to the compatibility test.
86
87
Makes sure that test won't fail on unrelated account (for example editor misconfiguration).
88
"""
89
compatibility_errors = [
90
"Error loading extension",
91
"Failed to load interface method",
92
'Parameter "mb" is null.',
93
'Parameter "bfi" is null.',
94
"Method bind not found:",
95
"Utility function not found:",
96
"has changed and no compatibility fallback has been provided",
97
"Failed to open file `builtin_methods.txt`",
98
"Failed to open file `class_methods.txt`",
99
"Failed to open file `utility_functions.txt`",
100
"Failed to open file `platform_methods.txt`",
101
"Outcome = FAILURE",
102
]
103
104
return any(compatibility_error in errors for compatibility_error in compatibility_errors)
105
106
107
def process_compatibility_test(proc: subprocess.Popen[bytes], timeout: int = 5) -> str | None:
108
"""
109
Returns the stderr output as a string, if any.
110
111
Terminates test if nothing has been written to stdout/stderr for specified time.
112
"""
113
errors = bytearray()
114
115
while True:
116
try:
117
_out, err = proc.communicate(timeout=timeout)
118
if err:
119
errors.extend(err)
120
except subprocess.TimeoutExpired:
121
proc.kill()
122
_out, err = proc.communicate()
123
if err:
124
errors.extend(err)
125
break
126
127
return errors.decode("utf-8") if errors else None
128
129
130
def compatibility_check(godot4_bin: str) -> bool:
131
"""
132
Checks if methods specified for previous Godot versions can be properly loaded with
133
the latest Godot4 binary.
134
"""
135
# A bit crude albeit working solution – use stderr to check for compatibility-related errors.
136
proc = subprocess.Popen(
137
[godot4_bin, "--headless", "-e", "--path", PROJECT_PATH],
138
stdout=subprocess.PIPE,
139
stderr=subprocess.PIPE,
140
)
141
142
if (errors := process_compatibility_test(proc)) and has_compatibility_test_failed(errors):
143
print(f"Compatibility test failed. Errors:\n {errors}")
144
return False
145
return True
146
147
148
if __name__ == "__main__":
149
godot4_bin = os.environ["GODOT4_BIN"]
150
reftags = os.environ["REFTAGS"].split(",")
151
is_success = True
152
for reftag in reftags:
153
generate_test_data_files(reftag)
154
if not compatibility_check(godot4_bin):
155
print(f"Compatibility test against Godot{reftag} failed")
156
is_success = False
157
remove_test_data_files()
158
159
if not is_success:
160
exit(1)
161
162