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
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:
58
modules/gdscript/tests/README.md
Normal file
58
modules/gdscript/tests/README.md
Normal 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
|
709
modules/gdscript/tests/gdscript_test_runner.cpp
Normal file
709
modules/gdscript/tests/gdscript_test_runner.cpp
Normal 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
|
137
modules/gdscript/tests/gdscript_test_runner.h
Normal file
137
modules/gdscript/tests/gdscript_test_runner.h
Normal 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
|
105
modules/gdscript/tests/gdscript_test_runner_suite.h
Normal file
105
modules/gdscript/tests/gdscript_test_runner_suite.h
Normal 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
|
11
modules/gdscript/tests/scripts/.editorconfig
Normal file
11
modules/gdscript/tests/scripts/.editorconfig
Normal 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
|
2
modules/gdscript/tests/scripts/.gitignore
vendored
Normal file
2
modules/gdscript/tests/scripts/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# Ignore metadata if someone open this on Godot.
|
||||
/.godot
|
@@ -0,0 +1,10 @@
|
||||
class A extends InstancePlaceholder:
|
||||
func _init():
|
||||
print('no')
|
||||
|
||||
class B extends A:
|
||||
pass
|
||||
|
||||
func test():
|
||||
InstancePlaceholder.new()
|
||||
B.new()
|
@@ -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.
|
@@ -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
|
@@ -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".
|
@@ -0,0 +1,6 @@
|
||||
var num := 1
|
||||
|
||||
@export_range(num, 10) var a
|
||||
|
||||
func test():
|
||||
pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Argument 1 of annotation "@export_range" isn't a constant expression.
|
@@ -0,0 +1,3 @@
|
||||
enum { V }
|
||||
func test():
|
||||
V = 1
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Cannot assign a new value to a constant.
|
@@ -0,0 +1,3 @@
|
||||
enum NamedEnum { V }
|
||||
func test():
|
||||
NamedEnum.V = 1
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Cannot assign a new value to a constant.
|
@@ -0,0 +1,4 @@
|
||||
signal your_base
|
||||
signal my_base
|
||||
func test():
|
||||
your_base = my_base
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 4: Cannot assign a new value to a constant.
|
@@ -0,0 +1,4 @@
|
||||
func test():
|
||||
var tree := SceneTree.new()
|
||||
tree.root = Window.new()
|
||||
tree.free()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Cannot assign a new value to a read-only property.
|
@@ -0,0 +1,4 @@
|
||||
func test():
|
||||
var state := PhysicsDirectBodyState3DExtension.new()
|
||||
state.center_of_mass.x += 1.0
|
||||
state.free()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Cannot assign a new value to a read-only property.
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
var var_color: String = Color.RED
|
||||
print('not ok')
|
@@ -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.
|
@@ -0,0 +1,4 @@
|
||||
signal my_signal()
|
||||
|
||||
func test():
|
||||
var _a := await my_signal
|
@@ -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.
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
# Error here.
|
||||
print(2.2 << 4)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Invalid operands to operator <<, float and int.
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
# Error here.
|
||||
print(2 >> 4.4)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Invalid operands to operator >>, int and float.
|
@@ -0,0 +1,7 @@
|
||||
# GH-73283
|
||||
|
||||
class MyClass:
|
||||
pass
|
||||
|
||||
func test():
|
||||
MyClass.not_existing_method()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 7: Static function "not_existing_method()" not found in base "MyClass".
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
var integer := 1
|
||||
print(integer as Array)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Invalid cast. Cannot convert from "int" to "Array".
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
var integer := 1
|
||||
print(integer as Node)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Invalid cast. Cannot convert from "int" to "Node".
|
@@ -0,0 +1,3 @@
|
||||
func test():
|
||||
var object := RefCounted.new()
|
||||
print(object as int)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Invalid cast. Cannot convert from "RefCounted" to "int".
|
@@ -0,0 +1,5 @@
|
||||
class Vector2:
|
||||
pass
|
||||
|
||||
func test():
|
||||
pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 1: Class "Vector2" hides a built-in type.
|
@@ -0,0 +1,5 @@
|
||||
const array: Array = [0]
|
||||
|
||||
func test():
|
||||
var key: int = 0
|
||||
array[key] = 0
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Cannot assign a new value to a constant.
|
@@ -0,0 +1,5 @@
|
||||
const dictionary := {}
|
||||
|
||||
func test():
|
||||
var key: int = 0
|
||||
dictionary[key] = 0
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Cannot assign a new value to a constant.
|
@@ -0,0 +1,4 @@
|
||||
const Vector2 = 0
|
||||
|
||||
func test():
|
||||
pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 1: The member "Vector2" cannot have the same name as a builtin type.
|
@@ -0,0 +1,5 @@
|
||||
const base := [0]
|
||||
|
||||
func test():
|
||||
var sub := base[0]
|
||||
if sub is String: pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Expression is of type "int" so it can't be of type "String".
|
@@ -0,0 +1,5 @@
|
||||
const CONSTANT = 25
|
||||
|
||||
|
||||
func test():
|
||||
CONSTANT(123)
|
@@ -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".
|
@@ -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()
|
@@ -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".
|
@@ -0,0 +1 @@
|
||||
@abstract class_name AbstractScript
|
@@ -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)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 10: Expression is of type "B" so it can't be of type "C".
|
@@ -0,0 +1,8 @@
|
||||
func test():
|
||||
print(InnerA.new())
|
||||
|
||||
class InnerA extends InnerB:
|
||||
pass
|
||||
|
||||
class InnerB extends InnerA:
|
||||
pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 4: Cyclic inheritance.
|
@@ -0,0 +1,5 @@
|
||||
func test():
|
||||
print(c1)
|
||||
|
||||
const c1 = c2
|
||||
const c2 = c1
|
@@ -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".
|
@@ -0,0 +1,5 @@
|
||||
func test():
|
||||
print(E1.V)
|
||||
|
||||
enum E1 {V = E2.V}
|
||||
enum E2 {V = E1.V}
|
@@ -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.
|
@@ -0,0 +1,5 @@
|
||||
func test():
|
||||
print(EV1)
|
||||
|
||||
enum {EV1 = EV2}
|
||||
enum {EV2 = EV1}
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Could not resolve member "EV1": Cyclic reference.
|
@@ -0,0 +1,6 @@
|
||||
func test():
|
||||
print(v)
|
||||
|
||||
var v = A.v
|
||||
|
||||
const A = preload("cyclic_ref_external_a.notest.gd")
|
@@ -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".
|
@@ -0,0 +1,5 @@
|
||||
class_name TestCyclicRefExternalA
|
||||
|
||||
const B = preload("cyclic_ref_external.gd")
|
||||
|
||||
var v = B.v
|
@@ -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
|
@@ -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.
|
@@ -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()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 11: Could not resolve member "f": Cyclic reference.
|
@@ -0,0 +1,5 @@
|
||||
func test():
|
||||
print(v1)
|
||||
|
||||
var v1 := v2
|
||||
var v2 := v1
|
@@ -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.
|
@@ -0,0 +1,4 @@
|
||||
var v1 = v1
|
||||
|
||||
func test():
|
||||
print(v1)
|
@@ -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".
|
@@ -0,0 +1,6 @@
|
||||
func test():
|
||||
var lua_dict = {
|
||||
a = 1,
|
||||
b = 2,
|
||||
a = 3, # Duplicate isn't allowed.
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).
|
@@ -0,0 +1,6 @@
|
||||
func test():
|
||||
var lua_dict_with_string = {
|
||||
a = 1,
|
||||
b = 2,
|
||||
"a" = 3, # Duplicate isn't allowed.
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).
|
@@ -0,0 +1,6 @@
|
||||
func test():
|
||||
var python_dict = {
|
||||
"a": 1,
|
||||
"b": 2,
|
||||
"a": 3, # Duplicate isn't allowed.
|
||||
}
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Key "a" was already used in this dictionary (at line 3).
|
@@ -0,0 +1,9 @@
|
||||
# https://github.com/godotengine/godot/issues/62957
|
||||
|
||||
func test():
|
||||
var dict = {
|
||||
&"key": "StringName",
|
||||
"key": "String"
|
||||
}
|
||||
|
||||
print("Invalid dictionary: %s" % dict)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 6: Key "key" was already used in this dictionary (at line 5).
|
@@ -0,0 +1,2 @@
|
||||
func test():
|
||||
Time.new()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 2: Cannot construct native class "Time" because it is an engine singleton.
|
@@ -0,0 +1,4 @@
|
||||
enum Enum {V1, V2}
|
||||
|
||||
func test():
|
||||
Enum.clear()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 4: Cannot call non-const Dictionary function "clear()" on enum "Enum".
|
@@ -0,0 +1,4 @@
|
||||
enum Enum {V1, V2}
|
||||
|
||||
func test():
|
||||
var bad = Enum.V3
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 4: Cannot find member "V3" in base "enum_bad_value.gd.Enum".
|
@@ -0,0 +1,2 @@
|
||||
func test():
|
||||
print(Vector3.Axis)
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 2: Type "Axis" in base "Vector3" cannot be used on its own.
|
@@ -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)
|
@@ -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".
|
@@ -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)
|
@@ -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.
|
@@ -0,0 +1,5 @@
|
||||
enum Enum {V1, V2}
|
||||
|
||||
func test():
|
||||
var Enum2 = Enum
|
||||
Enum2.clear()
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 5: Cannot call non-const Dictionary function "clear()" on enum "Enum".
|
@@ -0,0 +1,7 @@
|
||||
enum Size {
|
||||
# Error here. Enum values must be integers.
|
||||
S = 0.0,
|
||||
}
|
||||
|
||||
func test():
|
||||
pass
|
@@ -0,0 +1,2 @@
|
||||
GDTEST_ANALYZER_ERROR
|
||||
>> ERROR at line 3: Enum values must be integers.
|
@@ -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)
|
@@ -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
Reference in New Issue
Block a user