initial commit, 4.5 stable
Some checks failed
🔗 GHA / 📊 Static checks (push) Has been cancelled
🔗 GHA / 🤖 Android (push) Has been cancelled
🔗 GHA / 🍏 iOS (push) Has been cancelled
🔗 GHA / 🐧 Linux (push) Has been cancelled
🔗 GHA / 🍎 macOS (push) Has been cancelled
🔗 GHA / 🏁 Windows (push) Has been cancelled
🔗 GHA / 🌐 Web (push) Has been cancelled

This commit is contained in:
2025-09-16 20:46:46 -04:00
commit 9d30169a8d
13378 changed files with 7050105 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
# GDScript integration tests
The `scripts/` folder contains integration tests in the form of GDScript files
and output files.
See the
[Integration tests for GDScript documentation](https://docs.godotengine.org/en/latest/engine_details/architecture/unit_testing.html#integration-tests-for-gdscript)
for information about creating and running GDScript integration tests.
# GDScript Autocompletion tests
The `script/completion` folder contains test for the GDScript autocompletion.
Each test case consists of at least one `.gd` file, which contains the code, and one `.cfg` file, which contains expected results and configuration. Inside of the GDScript file the character `➡` represents the cursor position, at which autocompletion is invoked.
The script files won't be parsable GDScript since it contains an invalid char and often the code is not complete during autocompletion. To allow for a valid base when used with a scene, the
runner will remove the line which contains `➡`. Therefore the scripts need to be valid if this line is removed, otherwise the test might behave in unexpected ways. This may for example require
adding an additional `pass` statement.
This also means, that the runner will add the script to its owner node, so the script should not be loaded through the scene file.
The config file contains two section:
`[input]` contains keys that configure the test environment. The following keys are possible:
- `cs: boolean = false`: If `true`, the test will be skipped when running a non C# build.
- `use_single_quotes: boolean = false`: Configures the corresponding editor setting for the test.
- `add_node_path_literals: boolean = false`: Configures the corresponding editor setting for the test.
- `add_string_name_literals: boolean = false`: Configures the corresponding editor setting for the test.
- `scene: String`: Allows to specify a scene which is opened while autocompletion is performed. If this is not set the test runner will search for a `.tscn` file with the same basename as the GDScript file. If that isn't found either, autocompletion will behave as if no scene was opened.
- `node_path: String`: The node path of the node which holds the current script inside of the scene. Defaults to the scene root node.
`[output]` specifies the expected results for the test. The following key are supported:
- `include: Array`: An unordered list of suggestions that should be in the result. Each entry is one dictionary with the following keys: `display`, `insert_text`, `kind`, `location`, which correspond to the suggestion struct which is used in the code. The runner only tests against specified keys, so in most cases `display` will suffice.
- `exclude: Array`: An array of suggestions which should not be in the result. The entries take the same form as for `include`.
- `call_hint: String`: The expected call hint returned by autocompletion.
- `forced: boolean`: Whether autocompletion is expected to force opening a completion window.
Tests will only test against entries in `[output]` that were specified.
## Writing autocompletion tests
To avoid failing edge cases a certain behavior needs to be tested multiple times. Some things that tests should account for:
- All possible types: Test with all possible types that apply to the tested behavior. (For the last points testing against `SCRIPT` and `CLASS` should suffice. `CLASS` can be obtained through C#, `SCRIPT` through GDScript. Relying on autoloads to be of type `SCRIPT` is not good, since this might change in the future.)
- `BUILTIN`
- `NATIVE`
- GDScripts (with `class_name` as well as `preload`ed)
- C# (as standin for all other language bindings) (with `class_name` as well as `preload`ed)
- Autoloads
- Possible contexts: the completion might be placed in different places of the program. e.g:
- initializers of class members
- directly inside a suite
- assignments inside a suite
- as parameter to a call

View File

@@ -0,0 +1,709 @@
/**************************************************************************/
/* gdscript_test_runner.cpp */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#include "gdscript_test_runner.h"
#include "../gdscript.h"
#include "../gdscript_analyzer.h"
#include "../gdscript_compiler.h"
#include "../gdscript_parser.h"
#include "../gdscript_tokenizer_buffer.h"
#include "core/config/project_settings.h"
#include "core/core_globals.h"
#include "core/io/dir_access.h"
#include "core/io/file_access_pack.h"
#include "core/os/os.h"
#include "core/string/string_builder.h"
#include "scene/resources/packed_scene.h"
#include "tests/test_macros.h"
namespace GDScriptTests {
void init_autoloads() {
HashMap<StringName, ProjectSettings::AutoloadInfo> autoloads = ProjectSettings::get_singleton()->get_autoload_list();
// First pass, add the constants so they exist before any script is loaded.
for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
const ProjectSettings::AutoloadInfo &info = E.value;
if (info.is_singleton) {
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
ScriptServer::get_language(i)->add_global_constant(info.name, Variant());
}
}
}
// Second pass, load into global constants.
for (const KeyValue<StringName, ProjectSettings::AutoloadInfo> &E : ProjectSettings::get_singleton()->get_autoload_list()) {
const ProjectSettings::AutoloadInfo &info = E.value;
if (!info.is_singleton) {
// Skip non-singletons since we don't have a scene tree here anyway.
continue;
}
Node *n = nullptr;
if (ResourceLoader::get_resource_type(info.path) == "PackedScene") {
// Cache the scene reference before loading it (for cyclic references)
Ref<PackedScene> scn;
scn.instantiate();
scn->set_path(info.path);
scn->reload_from_file();
ERR_CONTINUE_MSG(scn.is_null(), vformat("Failed to instantiate an autoload, can't load from path: %s.", info.path));
if (scn.is_valid()) {
n = scn->instantiate();
}
} else {
Ref<Resource> res = ResourceLoader::load(info.path);
ERR_CONTINUE_MSG(res.is_null(), vformat("Failed to instantiate an autoload, can't load from path: %s.", info.path));
Ref<Script> scr = res;
if (scr.is_valid()) {
StringName ibt = scr->get_instance_base_type();
bool valid_type = ClassDB::is_parent_class(ibt, "Node");
ERR_CONTINUE_MSG(!valid_type, vformat("Failed to instantiate an autoload, script '%s' does not inherit from 'Node'.", info.path));
Object *obj = ClassDB::instantiate(ibt);
ERR_CONTINUE_MSG(!obj, vformat("Failed to instantiate an autoload, cannot instantiate '%s'.", ibt));
n = Object::cast_to<Node>(obj);
n->set_script(scr);
}
}
ERR_CONTINUE_MSG(!n, vformat("Failed to instantiate an autoload, path is not pointing to a scene or a script: %s.", info.path));
n->set_name(info.name);
for (int i = 0; i < ScriptServer::get_language_count(); i++) {
ScriptServer::get_language(i)->add_global_constant(info.name, n);
}
}
}
void init_language(const String &p_base_path) {
// Setup project settings since it's needed by the languages to get the global scripts.
// This also sets up the base resource path.
Error err = ProjectSettings::get_singleton()->setup(p_base_path, String(), true);
if (err) {
print_line("Could not load project settings.");
// Keep going since some scripts still work without this.
}
// Initialize the language for the test routine.
GDScriptLanguage::get_singleton()->init();
init_autoloads();
}
void finish_language() {
GDScriptLanguage::get_singleton()->finish();
ScriptServer::global_classes_clear();
}
StringName GDScriptTestRunner::test_function_name;
GDScriptTestRunner::GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames, bool p_use_binary_tokens) {
test_function_name = StringName("test");
do_init_languages = p_init_language;
print_filenames = p_print_filenames;
binary_tokens = p_use_binary_tokens;
source_dir = p_source_dir;
if (!source_dir.ends_with("/")) {
source_dir += "/";
}
if (do_init_languages) {
init_language(p_source_dir);
}
#ifdef DEBUG_ENABLED
// Set all warning levels to "Warn" in order to test them properly, even the ones that default to error.
ProjectSettings::get_singleton()->set_setting("debug/gdscript/warnings/enable", true);
for (int i = 0; i < (int)GDScriptWarning::WARNING_MAX; i++) {
if (i == GDScriptWarning::UNTYPED_DECLARATION || i == GDScriptWarning::INFERRED_DECLARATION) {
// TODO: Add ability for test scripts to specify which warnings to enable/disable for testing.
continue;
}
String warning_setting = GDScriptWarning::get_settings_path_from_code((GDScriptWarning::Code)i);
ProjectSettings::get_singleton()->set_setting(warning_setting, (int)GDScriptWarning::WARN);
}
#endif
// Enable printing to show results
CoreGlobals::print_line_enabled = true;
CoreGlobals::print_error_enabled = true;
}
GDScriptTestRunner::~GDScriptTestRunner() {
test_function_name = StringName();
if (do_init_languages) {
finish_language();
}
}
#ifndef DEBUG_ENABLED
static String strip_warnings(const String &p_expected) {
// On release builds we don't have warnings. Here we remove them from the output before comparison
// so it doesn't fail just because of difference in warnings.
String expected_no_warnings;
for (String line : p_expected.split("\n")) {
if (line.begins_with("~~ ")) {
continue;
}
expected_no_warnings += line + "\n";
}
return expected_no_warnings.strip_edges() + "\n";
}
#endif
int GDScriptTestRunner::run_tests() {
if (!make_tests()) {
FAIL("An error occurred while making the tests.");
return -1;
}
if (!generate_class_index()) {
FAIL("An error occurred while generating class index.");
return -1;
}
int failed = 0;
for (int i = 0; i < tests.size(); i++) {
GDScriptTest test = tests[i];
if (print_filenames) {
print_line(test.get_source_relative_filepath());
}
GDScriptTest::TestResult result = test.run_test();
String expected = FileAccess::get_file_as_string(test.get_output_file());
#ifndef DEBUG_ENABLED
expected = strip_warnings(expected);
#endif
INFO(test.get_source_file());
if (!result.passed) {
INFO(expected);
failed++;
}
CHECK_MESSAGE(result.passed, (result.passed ? String() : result.output));
}
return failed;
}
bool GDScriptTestRunner::generate_outputs() {
is_generating = true;
if (!make_tests()) {
print_line("Failed to generate a test output.");
return false;
}
if (!generate_class_index()) {
return false;
}
for (int i = 0; i < tests.size(); i++) {
GDScriptTest test = tests[i];
if (print_filenames) {
print_line(test.get_source_relative_filepath());
} else {
OS::get_singleton()->print(".");
}
bool result = test.generate_output();
if (!result) {
print_line("\nCould not generate output for " + test.get_source_file());
return false;
}
}
print_line("\nGenerated output files for " + itos(tests.size()) + " tests successfully.");
return true;
}
bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) {
Error err = OK;
Ref<DirAccess> dir(DirAccess::open(p_dir, &err));
if (err != OK) {
return false;
}
String current_dir = dir->get_current_dir();
dir->list_dir_begin();
String next = dir->get_next();
while (!next.is_empty()) {
if (dir->current_is_dir()) {
if (next == "." || next == ".." || next == "completion" || next == "lsp") {
next = dir->get_next();
continue;
}
if (!make_tests_for_dir(current_dir.path_join(next))) {
return false;
}
} else {
// `*.notest.gd` files are skipped.
if (next.ends_with(".notest.gd")) {
next = dir->get_next();
continue;
} else if (binary_tokens && next.ends_with(".textonly.gd")) {
next = dir->get_next();
continue;
} else if (next.get_extension().to_lower() == "gd") {
#ifndef DEBUG_ENABLED
// On release builds, skip tests marked as debug only.
Error open_err = OK;
Ref<FileAccess> script_file(FileAccess::open(current_dir.path_join(next), FileAccess::READ, &open_err));
if (open_err != OK) {
ERR_PRINT(vformat(R"(Couldn't open test file "%s".)", next));
next = dir->get_next();
continue;
} else {
if (script_file->get_line() == "#debug-only") {
next = dir->get_next();
continue;
}
}
#endif
String out_file = next.get_basename() + ".out";
ERR_FAIL_COND_V_MSG(!is_generating && !dir->file_exists(out_file), false, "Could not find output file for " + next);
if (next.ends_with(".bin.gd")) {
// Test text mode first.
GDScriptTest text_test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
tests.push_back(text_test);
// Test binary mode even without `--use-binary-tokens`.
GDScriptTest bin_test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
bin_test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER);
tests.push_back(bin_test);
} else {
GDScriptTest test(current_dir.path_join(next), current_dir.path_join(out_file), source_dir);
if (binary_tokens) {
test.set_tokenizer_mode(GDScriptTest::TOKENIZER_BUFFER);
}
tests.push_back(test);
}
}
}
next = dir->get_next();
}
dir->list_dir_end();
return true;
}
bool GDScriptTestRunner::make_tests() {
Error err = OK;
Ref<DirAccess> dir(DirAccess::open(source_dir, &err));
ERR_FAIL_COND_V_MSG(err != OK, false, "Could not open specified test directory.");
source_dir = dir->get_current_dir() + "/"; // Make it absolute path.
return make_tests_for_dir(dir->get_current_dir());
}
static bool generate_class_index_recursive(const String &p_dir) {
Error err = OK;
Ref<DirAccess> dir(DirAccess::open(p_dir, &err));
if (err != OK) {
return false;
}
String current_dir = dir->get_current_dir();
dir->list_dir_begin();
String next = dir->get_next();
StringName gdscript_name = GDScriptLanguage::get_singleton()->get_name();
while (!next.is_empty()) {
if (dir->current_is_dir()) {
if (next == "." || next == ".." || next == "completion" || next == "lsp") {
next = dir->get_next();
continue;
}
if (!generate_class_index_recursive(current_dir.path_join(next))) {
return false;
}
} else {
if (!next.ends_with(".gd")) {
next = dir->get_next();
continue;
}
String base_type;
String source_file = current_dir.path_join(next);
bool is_abstract = false;
bool is_tool = false;
String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type, nullptr, &is_abstract, &is_tool);
if (class_name.is_empty()) {
next = dir->get_next();
continue;
}
ERR_FAIL_COND_V_MSG(ScriptServer::is_global_class(class_name), false,
"Class name '" + class_name + "' from " + source_file + " is already used in " + ScriptServer::get_global_class_path(class_name));
ScriptServer::add_global_class(class_name, base_type, gdscript_name, source_file, is_abstract, is_tool);
}
next = dir->get_next();
}
dir->list_dir_end();
return true;
}
bool GDScriptTestRunner::generate_class_index() {
Error err = OK;
Ref<DirAccess> dir(DirAccess::open(source_dir, &err));
ERR_FAIL_COND_V_MSG(err != OK, false, "Could not open specified test directory.");
source_dir = dir->get_current_dir() + "/"; // Make it absolute path.
return generate_class_index_recursive(dir->get_current_dir());
}
GDScriptTest::GDScriptTest(const String &p_source_path, const String &p_output_path, const String &p_base_dir) {
source_file = p_source_path;
output_file = p_output_path;
base_dir = p_base_dir;
_print_handler.printfunc = print_handler;
_error_handler.errfunc = error_handler;
}
void GDScriptTestRunner::handle_cmdline() {
List<String> cmdline_args = OS::get_singleton()->get_cmdline_args();
for (List<String>::Element *E = cmdline_args.front(); E; E = E->next()) {
String &cmd = E->get();
if (cmd == "--gdscript-generate-tests") {
String path;
if (E->next()) {
path = E->next()->get();
} else {
path = "modules/gdscript/tests/scripts";
}
GDScriptTestRunner runner(path, false, cmdline_args.find("--print-filenames") != nullptr);
bool completed = runner.generate_outputs();
int failed = completed ? 0 : -1;
exit(failed);
}
}
}
void GDScriptTest::enable_stdout() {
// TODO: this could likely be handled by doctest or `tests/test_macros.h`.
OS::get_singleton()->set_stdout_enabled(true);
OS::get_singleton()->set_stderr_enabled(true);
}
void GDScriptTest::disable_stdout() {
// TODO: this could likely be handled by doctest or `tests/test_macros.h`.
OS::get_singleton()->set_stdout_enabled(false);
OS::get_singleton()->set_stderr_enabled(false);
}
void GDScriptTest::print_handler(void *p_this, const String &p_message, bool p_error, bool p_rich) {
TestResult *result = (TestResult *)p_this;
result->output += p_message + "\n";
}
void GDScriptTest::error_handler(void *p_this, const char *p_function, const char *p_file, int p_line, const char *p_error, const char *p_explanation, bool p_editor_notify, ErrorHandlerType p_type) {
ErrorHandlerData *data = (ErrorHandlerData *)p_this;
GDScriptTest *self = data->self;
TestResult *result = data->result;
result->status = GDTEST_RUNTIME_ERROR;
String header = _error_handler_type_string(p_type);
// Only include the file, line, and function for script errors,
// otherwise the test outputs changes based on the platform/compiler.
if (p_type == ERR_HANDLER_SCRIPT) {
header += vformat(" at %s:%d on %s()",
String::utf8(p_file).trim_prefix(self->base_dir).replace_char('\\', '/'),
p_line,
String::utf8(p_function));
}
StringBuilder error_string;
error_string.append(vformat(">> %s: %s\n", header, String::utf8(p_error)));
if (strlen(p_explanation) > 0) {
error_string.append(vformat(">> %s\n", String::utf8(p_explanation)));
}
result->output += error_string.as_string();
}
bool GDScriptTest::check_output(const String &p_output) const {
Error err = OK;
String expected = FileAccess::get_file_as_string(output_file, &err);
ERR_FAIL_COND_V_MSG(err != OK, false, "Error when opening the output file.");
String got = p_output.strip_edges(); // TODO: may be hacky.
got += "\n"; // Make sure to insert newline for CI static checks.
#ifndef DEBUG_ENABLED
expected = strip_warnings(expected);
#endif
return got == expected;
}
String GDScriptTest::get_text_for_status(GDScriptTest::TestStatus p_status) const {
switch (p_status) {
case GDTEST_OK:
return "GDTEST_OK";
case GDTEST_LOAD_ERROR:
return "GDTEST_LOAD_ERROR";
case GDTEST_PARSER_ERROR:
return "GDTEST_PARSER_ERROR";
case GDTEST_ANALYZER_ERROR:
return "GDTEST_ANALYZER_ERROR";
case GDTEST_COMPILER_ERROR:
return "GDTEST_COMPILER_ERROR";
case GDTEST_RUNTIME_ERROR:
return "GDTEST_RUNTIME_ERROR";
}
return "";
}
GDScriptTest::TestResult GDScriptTest::execute_test_code(bool p_is_generating) {
disable_stdout();
TestResult result;
result.status = GDTEST_OK;
result.output = String();
result.passed = false;
Error err = OK;
// Create script.
Ref<GDScript> script;
script.instantiate();
script->set_path(source_file);
if (tokenizer_mode == TOKENIZER_TEXT) {
err = script->load_source_code(source_file);
} else {
String code = FileAccess::get_file_as_string(source_file, &err);
if (!err) {
Vector<uint8_t> buffer = GDScriptTokenizerBuffer::parse_code_string(code, GDScriptTokenizerBuffer::COMPRESS_ZSTD);
script->set_binary_tokens_source(buffer);
}
}
if (err != OK) {
enable_stdout();
result.status = GDTEST_LOAD_ERROR;
result.passed = false;
ERR_FAIL_V_MSG(result, "\nCould not load source code for: '" + source_file + "'");
}
// Test parsing.
GDScriptParser parser;
if (tokenizer_mode == TOKENIZER_TEXT) {
err = parser.parse(script->get_source_code(), source_file, false);
} else {
err = parser.parse_binary(script->get_binary_tokens_source(), source_file);
}
if (err != OK) {
enable_stdout();
result.status = GDTEST_PARSER_ERROR;
result.output = get_text_for_status(result.status) + "\n";
const List<GDScriptParser::ParserError> &errors = parser.get_errors();
if (!errors.is_empty()) {
// Only the first error since the following might be cascading.
result.output += errors.front()->get().message + "\n"; // TODO: line, column?
}
if (!p_is_generating) {
result.passed = check_output(result.output);
}
return result;
}
// Test type-checking.
GDScriptAnalyzer analyzer(&parser);
err = analyzer.analyze();
if (err != OK) {
enable_stdout();
result.status = GDTEST_ANALYZER_ERROR;
result.output = get_text_for_status(result.status) + "\n";
StringBuilder error_string;
for (const GDScriptParser::ParserError &error : parser.get_errors()) {
error_string.append(vformat(">> ERROR at line %d: %s\n", error.line, error.message));
}
result.output += error_string.as_string();
if (!p_is_generating) {
result.passed = check_output(result.output);
}
return result;
}
#ifdef DEBUG_ENABLED
StringBuilder warning_string;
for (const GDScriptWarning &warning : parser.get_warnings()) {
warning_string.append(vformat("~~ WARNING at line %d: (%s) %s\n", warning.start_line, warning.get_name(), warning.get_message()));
}
result.output += warning_string.as_string();
#endif
// Test compiling.
GDScriptCompiler compiler;
err = compiler.compile(&parser, script.ptr(), false);
if (err != OK) {
enable_stdout();
result.status = GDTEST_COMPILER_ERROR;
result.output = get_text_for_status(result.status) + "\n";
result.output += compiler.get_error() + "\n";
if (!p_is_generating) {
result.passed = check_output(result.output);
}
return result;
}
// `*.norun.gd` files are allowed to not contain a `test()` function (no runtime testing).
if (source_file.ends_with(".norun.gd")) {
enable_stdout();
result.status = GDTEST_OK;
result.output = get_text_for_status(result.status) + "\n" + result.output;
if (!p_is_generating) {
result.passed = check_output(result.output);
}
return result;
}
// Test running.
const HashMap<StringName, GDScriptFunction *>::ConstIterator test_function_element = script->get_member_functions().find(GDScriptTestRunner::test_function_name);
if (!test_function_element) {
enable_stdout();
result.status = GDTEST_LOAD_ERROR;
result.output = "";
result.passed = false;
ERR_FAIL_V_MSG(result, "\nCould not find test function on: '" + source_file + "'");
}
// Setup output handlers.
ErrorHandlerData error_data(&result, this);
_print_handler.userdata = &result;
_error_handler.userdata = &error_data;
add_print_handler(&_print_handler);
add_error_handler(&_error_handler);
err = script->reload();
if (err) {
enable_stdout();
result.status = GDTEST_LOAD_ERROR;
result.output = "";
result.passed = false;
remove_print_handler(&_print_handler);
remove_error_handler(&_error_handler);
ERR_FAIL_V_MSG(result, "\nCould not reload script: '" + source_file + "'");
}
// Create object instance for test.
Object *obj = ClassDB::instantiate(script->get_native()->get_name());
Ref<RefCounted> obj_ref;
if (obj->is_ref_counted()) {
obj_ref = Ref<RefCounted>(Object::cast_to<RefCounted>(obj));
}
obj->set_script(script);
GDScriptInstance *instance = static_cast<GDScriptInstance *>(obj->get_script_instance());
// Call test function.
Callable::CallError call_err;
instance->callp(GDScriptTestRunner::test_function_name, nullptr, 0, call_err);
// Tear down output handlers.
remove_print_handler(&_print_handler);
remove_error_handler(&_error_handler);
// Check results.
if (call_err.error != Callable::CallError::CALL_OK) {
enable_stdout();
result.status = GDTEST_LOAD_ERROR;
result.passed = false;
ERR_FAIL_V_MSG(result, "\nCould not call test function on: '" + source_file + "'");
}
result.output = get_text_for_status(result.status) + "\n" + result.output;
if (!p_is_generating) {
result.passed = check_output(result.output);
}
if (obj_ref.is_null()) {
memdelete(obj);
}
enable_stdout();
GDScriptCache::remove_script(script->get_path());
return result;
}
GDScriptTest::TestResult GDScriptTest::run_test() {
return execute_test_code(false);
}
bool GDScriptTest::generate_output() {
TestResult result = execute_test_code(true);
if (result.status == GDTEST_LOAD_ERROR) {
return false;
}
Error err = OK;
Ref<FileAccess> out_file = FileAccess::open(output_file, FileAccess::WRITE, &err);
if (err != OK) {
return false;
}
String output = result.output.strip_edges(); // TODO: may be hacky.
output += "\n"; // Make sure to insert newline for CI static checks.
out_file->store_string(output);
return true;
}
} // namespace GDScriptTests

View File

@@ -0,0 +1,137 @@
/**************************************************************************/
/* gdscript_test_runner.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "../gdscript.h"
#include "core/error/error_macros.h"
#include "core/string/print_string.h"
#include "core/string/ustring.h"
#include "core/templates/vector.h"
namespace GDScriptTests {
void init_autoloads();
void init_language(const String &p_base_path);
void finish_language();
// Single test instance in a suite.
class GDScriptTest {
public:
enum TestStatus {
GDTEST_OK,
GDTEST_LOAD_ERROR,
GDTEST_PARSER_ERROR,
GDTEST_ANALYZER_ERROR,
GDTEST_COMPILER_ERROR,
GDTEST_RUNTIME_ERROR,
};
struct TestResult {
TestStatus status;
String output;
bool passed;
};
enum TokenizerMode {
TOKENIZER_TEXT,
TOKENIZER_BUFFER,
};
private:
struct ErrorHandlerData {
TestResult *result = nullptr;
GDScriptTest *self = nullptr;
ErrorHandlerData(TestResult *p_result, GDScriptTest *p_this) {
result = p_result;
self = p_this;
}
};
String source_file;
String output_file;
String base_dir;
PrintHandlerList _print_handler;
ErrorHandlerList _error_handler;
TokenizerMode tokenizer_mode = TOKENIZER_TEXT;
void enable_stdout();
void disable_stdout();
bool check_output(const String &p_output) const;
String get_text_for_status(TestStatus p_status) const;
TestResult execute_test_code(bool p_is_generating);
public:
static void print_handler(void *p_this, const String &p_message, bool p_error, bool p_rich);
static void error_handler(void *p_this, const char *p_function, const char *p_file, int p_line, const char *p_error, const char *p_explanation, bool p_editor_notify, ErrorHandlerType p_type);
TestResult run_test();
bool generate_output();
const String &get_source_file() const { return source_file; }
const String get_source_relative_filepath() const { return source_file.trim_prefix(base_dir); }
const String &get_output_file() const { return output_file; }
void set_tokenizer_mode(TokenizerMode p_tokenizer_mode) { tokenizer_mode = p_tokenizer_mode; }
TokenizerMode get_tokenizer_mode() const { return tokenizer_mode; }
GDScriptTest(const String &p_source_path, const String &p_output_path, const String &p_base_dir);
GDScriptTest() :
GDScriptTest(String(), String(), String()) {} // Needed to use in Vector.
};
class GDScriptTestRunner {
String source_dir;
Vector<GDScriptTest> tests;
bool is_generating = false;
bool do_init_languages = false;
bool print_filenames; // Whether filenames should be printed when generated/running tests
bool binary_tokens; // Test with buffer tokenizer.
bool make_tests();
bool make_tests_for_dir(const String &p_dir);
bool generate_class_index();
public:
static StringName test_function_name;
static void handle_cmdline();
int run_tests();
bool generate_outputs();
GDScriptTestRunner(const String &p_source_dir, bool p_init_language, bool p_print_filenames = false, bool p_use_binary_tokens = false);
~GDScriptTestRunner();
};
} // namespace GDScriptTests

View File

@@ -0,0 +1,105 @@
/**************************************************************************/
/* gdscript_test_runner_suite.h */
/**************************************************************************/
/* This file is part of: */
/* GODOT ENGINE */
/* https://godotengine.org */
/**************************************************************************/
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
/* */
/* Permission is hereby granted, free of charge, to any person obtaining */
/* a copy of this software and associated documentation files (the */
/* "Software"), to deal in the Software without restriction, including */
/* without limitation the rights to use, copy, modify, merge, publish, */
/* distribute, sublicense, and/or sell copies of the Software, and to */
/* permit persons to whom the Software is furnished to do so, subject to */
/* the following conditions: */
/* */
/* The above copyright notice and this permission notice shall be */
/* included in all copies or substantial portions of the Software. */
/* */
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/**************************************************************************/
#pragma once
#include "gdscript_test_runner.h"
#include "tests/test_macros.h"
namespace GDScriptTests {
// TODO: Handle some cases failing on release builds. See: https://github.com/godotengine/godot/pull/88452
#ifdef TOOLS_ENABLED
TEST_SUITE("[Modules][GDScript]") {
TEST_CASE("Script compilation and runtime") {
bool print_filenames = OS::get_singleton()->get_cmdline_args().find("--print-filenames") != nullptr;
bool use_binary_tokens = OS::get_singleton()->get_cmdline_args().find("--use-binary-tokens") != nullptr;
GDScriptTestRunner runner("modules/gdscript/tests/scripts", true, print_filenames, use_binary_tokens);
int fail_count = runner.run_tests();
INFO("Make sure `*.out` files have expected results.");
REQUIRE_MESSAGE(fail_count == 0, "All GDScript tests should pass.");
}
}
#endif // TOOLS_ENABLED
TEST_CASE("[Modules][GDScript] Load source code dynamically and run it") {
GDScriptLanguage::get_singleton()->init();
Ref<GDScript> gdscript = memnew(GDScript);
gdscript->set_source_code(R"(
extends RefCounted
func _init():
set_meta("result", 42)
)");
// A spurious `Condition "err" is true` message is printed (despite parsing being successful and returning `OK`).
// Silence it.
ERR_PRINT_OFF;
const Error error = gdscript->reload();
ERR_PRINT_ON;
CHECK_MESSAGE(error == OK, "The script should parse successfully.");
// Run the script by assigning it to a reference-counted object.
Ref<RefCounted> ref_counted = memnew(RefCounted);
ref_counted->set_script(gdscript);
CHECK_MESSAGE(int(ref_counted->get_meta("result")) == 42, "The script should assign object metadata successfully.");
}
TEST_CASE("[Modules][GDScript] Validate built-in API") {
GDScriptLanguage *lang = GDScriptLanguage::get_singleton();
// Validate methods.
List<MethodInfo> builtin_methods;
lang->get_public_functions(&builtin_methods);
SUBCASE("[Modules][GDScript] Validate built-in methods") {
for (const MethodInfo &mi : builtin_methods) {
for (int64_t i = 0; i < mi.arguments.size(); ++i) {
TEST_COND((mi.arguments[i].name.is_empty() || mi.arguments[i].name.begins_with("_unnamed_arg")),
vformat("Unnamed argument in position %d of built-in method '%s'.", i, mi.name));
}
}
}
// Validate annotations.
List<MethodInfo> builtin_annotations;
lang->get_public_annotations(&builtin_annotations);
SUBCASE("[Modules][GDScript] Validate built-in annotations") {
for (const MethodInfo &ai : builtin_annotations) {
for (int64_t i = 0; i < ai.arguments.size(); ++i) {
TEST_COND((ai.arguments[i].name.is_empty() || ai.arguments[i].name.begins_with("_unnamed_arg")),
vformat("Unnamed argument in position %d of built-in annotation '%s'.", i, ai.name));
}
}
}
}
} // namespace GDScriptTests

View File

@@ -0,0 +1,11 @@
# Some tests handle invalid syntax deliberately; exclude relevant attributes.
# See also the `file-format` section in `.pre-commit-config.yaml`.
[parser/features/mixed_indentation_on_blank_lines.gd]
trim_trailing_whitespace = false
[parser/warnings/empty_file_newline.norun.gd]
insert_final_newline = false
[parser/warnings/empty_file_newline_comment.norun.gd]
insert_final_newline = false

View File

@@ -0,0 +1,2 @@
# Ignore metadata if someone open this on Godot.
/.godot

View File

@@ -0,0 +1,10 @@
class A extends InstancePlaceholder:
func _init():
print('no')
class B extends A:
pass
func test():
InstancePlaceholder.new()
B.new()

View File

@@ -0,0 +1,5 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 9: Native class "InstancePlaceholder" cannot be constructed as it is abstract.
>> ERROR at line 9: Name "new" is a Callable. You can call it with "new.call()" instead.
>> ERROR at line 10: Class "abstract_class_instantiate.gd::B" cannot be constructed as it is based on abstract native class "InstancePlaceholder".
>> ERROR at line 10: Name "new" is a Callable. You can call it with "new.call()" instead.

View File

@@ -0,0 +1,44 @@
@abstract class AbstractClass:
@abstract func some_func()
class ImplementedClass extends AbstractClass:
func some_func():
pass
@abstract class AbstractClassAgain extends ImplementedClass:
@abstract func some_func()
class Test1:
@abstract func some_func()
class Test2 extends AbstractClass:
pass
class Test3 extends AbstractClassAgain:
pass
class Test4 extends AbstractClass:
func some_func():
super()
func other_func():
super.some_func()
@abstract class A:
@abstract @abstract func abstract_dup()
# An abstract function cannot have a body.
@abstract func abstract_bodyful():
pass
# A static function cannot be marked as `@abstract`.
@abstract static func abstract_stat()
@abstract @abstract class DuplicateAbstract:
pass
func holding_some_invalid_lambda(invalid_default_arg = func():):
var some_invalid_lambda = (func():)
func test():
pass

View File

@@ -0,0 +1,13 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 37: "@abstract" annotation can only be used once per class.
>> ERROR at line 28: "@abstract" annotation can only be used once per function.
>> ERROR at line 35: "@abstract" annotation cannot be applied to static functions.
>> ERROR at line 40: A lambda function must have a ":" followed by a body.
>> ERROR at line 41: A lambda function must have a ":" followed by a body.
>> ERROR at line 11: Class "Test1" is not abstract but contains abstract methods. Mark the class as "@abstract" or remove "@abstract" from all methods in this class.
>> ERROR at line 14: Class "Test2" must implement "AbstractClass.some_func()" and other inherited abstract methods or be marked as "@abstract".
>> ERROR at line 17: Class "Test3" must implement "AbstractClassAgain.some_func()" and other inherited abstract methods or be marked as "@abstract".
>> ERROR at line 22: Cannot call the parent class' abstract function "some_func()" because it hasn't been defined.
>> ERROR at line 25: Cannot call the parent class' abstract function "some_func()" because it hasn't been defined.
>> ERROR at line 32: An abstract function cannot have a body.
>> ERROR at line 35: A function must either have a ":" followed by a body, or be marked as "@abstract".

View File

@@ -0,0 +1,6 @@
var num := 1
@export_range(num, 10) var a
func test():
pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Argument 1 of annotation "@export_range" isn't a constant expression.

View File

@@ -0,0 +1,3 @@
enum { V }
func test():
V = 1

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Cannot assign a new value to a constant.

View File

@@ -0,0 +1,3 @@
enum NamedEnum { V }
func test():
NamedEnum.V = 1

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Cannot assign a new value to a constant.

View File

@@ -0,0 +1,4 @@
signal your_base
signal my_base
func test():
your_base = my_base

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Cannot assign a new value to a constant.

View File

@@ -0,0 +1,4 @@
func test():
var tree := SceneTree.new()
tree.root = Window.new()
tree.free()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Cannot assign a new value to a read-only property.

View File

@@ -0,0 +1,4 @@
func test():
var state := PhysicsDirectBodyState3DExtension.new()
state.center_of_mass.x += 1.0
state.free()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Cannot assign a new value to a read-only property.

View File

@@ -0,0 +1,3 @@
func test():
var var_color: String = Color.RED
print('not ok')

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 2: Cannot assign a value of type "Color" as "String".
>> ERROR at line 2: Cannot assign a value of type Color to variable "var_color" with specified type String.

View File

@@ -0,0 +1,4 @@
signal my_signal()
func test():
var _a := await my_signal

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Cannot infer the type of "_a" variable because the value doesn't have a set type.

View File

@@ -0,0 +1,3 @@
func test():
# Error here.
print(2.2 << 4)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Invalid operands to operator <<, float and int.

View File

@@ -0,0 +1,3 @@
func test():
# Error here.
print(2 >> 4.4)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Invalid operands to operator >>, int and float.

View File

@@ -0,0 +1,7 @@
# GH-73283
class MyClass:
pass
func test():
MyClass.not_existing_method()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 7: Static function "not_existing_method()" not found in base "MyClass".

View File

@@ -0,0 +1,3 @@
func test():
var integer := 1
print(integer as Array)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Invalid cast. Cannot convert from "int" to "Array".

View File

@@ -0,0 +1,3 @@
func test():
var integer := 1
print(integer as Node)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Invalid cast. Cannot convert from "int" to "Node".

View File

@@ -0,0 +1,3 @@
func test():
var object := RefCounted.new()
print(object as int)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Invalid cast. Cannot convert from "RefCounted" to "int".

View File

@@ -0,0 +1,5 @@
class Vector2:
pass
func test():
pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 1: Class "Vector2" hides a built-in type.

View File

@@ -0,0 +1,5 @@
const array: Array = [0]
func test():
var key: int = 0
array[key] = 0

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Cannot assign a new value to a constant.

View File

@@ -0,0 +1,5 @@
const dictionary := {}
func test():
var key: int = 0
dictionary[key] = 0

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Cannot assign a new value to a constant.

View File

@@ -0,0 +1,4 @@
const Vector2 = 0
func test():
pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 1: The member "Vector2" cannot have the same name as a builtin type.

View File

@@ -0,0 +1,5 @@
const base := [0]
func test():
var sub := base[0]
if sub is String: pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Expression is of type "int" so it can't be of type "String".

View File

@@ -0,0 +1,5 @@
const CONSTANT = 25
func test():
CONSTANT(123)

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Member "CONSTANT" is not a function.
>> ERROR at line 5: Name "CONSTANT" called as a function but is a "int".

View File

@@ -0,0 +1,10 @@
extends RefCounted
const AbstractScript = preload("./construct_abstract_script.notest.gd")
@abstract class AbstractClass:
pass
func test():
var _a := AbstractScript.new()
var _b := AbstractClass.new()

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 9: Cannot construct abstract class "AbstractScript".
>> ERROR at line 10: Cannot construct abstract class "AbstractClass".

View File

@@ -0,0 +1 @@
@abstract class_name AbstractScript

View File

@@ -0,0 +1,10 @@
class A:
func _init():
pass
class B extends A: pass
class C extends A: pass
func test():
var x := B.new()
print(x is C)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 10: Expression is of type "B" so it can't be of type "C".

View File

@@ -0,0 +1,8 @@
func test():
print(InnerA.new())
class InnerA extends InnerB:
pass
class InnerB extends InnerA:
pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Cyclic inheritance.

View File

@@ -0,0 +1,5 @@
func test():
print(c1)
const c1 = c2
const c2 = c1

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Could not resolve member "c1": Cyclic reference.
>> ERROR at line 5: Could not resolve type for constant "c2".

View File

@@ -0,0 +1,5 @@
func test():
print(E1.V)
enum E1 {V = E2.V}
enum E2 {V = E1.V}

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Could not resolve member "E1": Cyclic reference.
>> ERROR at line 5: Enum values must be constant.

View File

@@ -0,0 +1,5 @@
func test():
print(EV1)
enum {EV1 = EV2}
enum {EV2 = EV1}

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Could not resolve member "EV1": Cyclic reference.

View File

@@ -0,0 +1,6 @@
func test():
print(v)
var v = A.v
const A = preload("cyclic_ref_external_a.notest.gd")

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Could not resolve external class member "v".
>> ERROR at line 4: Cannot find member "v" in base "TestCyclicRefExternalA".

View File

@@ -0,0 +1,5 @@
class_name TestCyclicRefExternalA
const B = preload("cyclic_ref_external.gd")
var v = B.v

View File

@@ -0,0 +1,9 @@
func test():
print(f1())
print(f2())
static func f1(p := f2()) -> int:
return 1
static func f2(p := f1()) -> int:
return 2

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 8: Could not resolve member "f1": Cyclic reference.
>> ERROR at line 8: Cannot infer the type of "p" parameter because the value doesn't have a set type.

View File

@@ -0,0 +1,12 @@
func test():
print(v)
var v := InnerA.new().f()
class InnerA:
func f(p := InnerB.new().f()) -> int:
return 1
class InnerB extends InnerA:
func f(p := 1) -> int:
return super.f()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 11: Could not resolve member "f": Cyclic reference.

View File

@@ -0,0 +1,5 @@
func test():
print(v1)
var v1 := v2
var v2 := v1

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Could not resolve member "v1": Cyclic reference.
>> ERROR at line 5: Cannot infer the type of "v2" variable because the value doesn't have a set type.

View File

@@ -0,0 +1,4 @@
var v1 = v1
func test():
print(v1)

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 1: Could not resolve member "v1": Cyclic reference.
>> ERROR at line 1: Could not resolve type for variable "v1".

View File

@@ -0,0 +1,6 @@
func test():
var lua_dict = {
a = 1,
b = 2,
a = 3, # Duplicate isn't allowed.
}

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).

View File

@@ -0,0 +1,6 @@
func test():
var lua_dict_with_string = {
a = 1,
b = 2,
"a" = 3, # Duplicate isn't allowed.
}

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).

View File

@@ -0,0 +1,6 @@
func test():
var python_dict = {
"a": 1,
"b": 2,
"a": 3, # Duplicate isn't allowed.
}

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).

View File

@@ -0,0 +1,9 @@
# https://github.com/godotengine/godot/issues/62957
func test():
var dict = {
&"key": "StringName",
"key": "String"
}
print("Invalid dictionary: %s" % dict)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 6: Key "key" was already used in this dictionary (at line 5).

View File

@@ -0,0 +1,2 @@
func test():
Time.new()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 2: Cannot construct native class "Time" because it is an engine singleton.

View File

@@ -0,0 +1,4 @@
enum Enum {V1, V2}
func test():
Enum.clear()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Cannot call non-const Dictionary function "clear()" on enum "Enum".

View File

@@ -0,0 +1,4 @@
enum Enum {V1, V2}
func test():
var bad = Enum.V3

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 4: Cannot find member "V3" in base "enum_bad_value.gd.Enum".

View File

@@ -0,0 +1,2 @@
func test():
print(Vector3.Axis)

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 2: Type "Axis" in base "Vector3" cannot be used on its own.

View File

@@ -0,0 +1,10 @@
enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 }
enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 }
# Different enum types can't be assigned without casting.
var class_var: MyEnum = MyEnum.ENUM_VALUE_1
func test():
print(class_var)
class_var = MyOtherEnum.OTHER_ENUM_VALUE_2
print(class_var)

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 9: Cannot assign a value of type "enum_class_var_assign_with_wrong_enum_type.gd.MyOtherEnum" as "enum_class_var_assign_with_wrong_enum_type.gd.MyEnum".
>> ERROR at line 9: Value of type "enum_class_var_assign_with_wrong_enum_type.gd.MyOtherEnum" cannot be assigned to a variable of type "enum_class_var_assign_with_wrong_enum_type.gd.MyEnum".

View File

@@ -0,0 +1,8 @@
enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 }
enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 }
# Different enum types can't be assigned without casting.
var class_var: MyEnum = MyOtherEnum.OTHER_ENUM_VALUE_1
func test():
print(class_var)

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Cannot assign a value of type "enum_class_var_init_with_wrong_enum_type.gd.MyOtherEnum" as "enum_class_var_init_with_wrong_enum_type.gd.MyEnum".
>> ERROR at line 5: Cannot assign a value of type enum_class_var_init_with_wrong_enum_type.gd.MyOtherEnum to variable "class_var" with specified type enum_class_var_init_with_wrong_enum_type.gd.MyEnum.

View File

@@ -0,0 +1,5 @@
enum Enum {V1, V2}
func test():
var Enum2 = Enum
Enum2.clear()

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 5: Cannot call non-const Dictionary function "clear()" on enum "Enum".

View File

@@ -0,0 +1,7 @@
enum Size {
# Error here. Enum values must be integers.
S = 0.0,
}
func test():
pass

View File

@@ -0,0 +1,2 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 3: Enum values must be integers.

View File

@@ -0,0 +1,8 @@
enum MyEnum { ENUM_VALUE_1, ENUM_VALUE_2 }
enum MyOtherEnum { OTHER_ENUM_VALUE_1, OTHER_ENUM_VALUE_2 }
func enum_func(e: MyEnum) -> void:
print(e)
func test():
enum_func(MyOtherEnum.OTHER_ENUM_VALUE_1)

View File

@@ -0,0 +1,3 @@
GDTEST_ANALYZER_ERROR
>> ERROR at line 8: Cannot pass a value of type "enum_function_parameter_wrong_type.gd.MyOtherEnum" as "enum_function_parameter_wrong_type.gd.MyEnum".
>> ERROR at line 8: Invalid argument for "enum_func()" function: argument 1 should be "enum_function_parameter_wrong_type.gd.MyEnum" but is "enum_function_parameter_wrong_type.gd.MyOtherEnum".

Some files were not shown because too many files have changed in this diff Show More