Add unit test runner for autocompletion
This commit is contained in:
parent
d822fd5322
commit
af4cbaf751
15 changed files with 236 additions and 12 deletions
|
@ -266,7 +266,7 @@ bool GDScriptTestRunner::make_tests_for_dir(const String &p_dir) {
|
|||
|
||||
while (!next.is_empty()) {
|
||||
if (dir->current_is_dir()) {
|
||||
if (next == "." || next == "..") {
|
||||
if (next == "." || next == ".." || next == "completion" || next == "lsp") {
|
||||
next = dir->get_next();
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[output]
|
||||
expected=[
|
||||
{"display": "autoplay"},
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
extends Node
|
||||
|
||||
var test: AnimationPlayer = $AnimationPlayer
|
||||
|
||||
func _ready():
|
||||
test.➡
|
|
@ -3,7 +3,7 @@
|
|||
; It also helps for opening Godot to edit the scripts, but please don't
|
||||
; let the editor changes be saved.
|
||||
|
||||
config_version=4
|
||||
config_version=5
|
||||
|
||||
[application]
|
||||
|
||||
|
|
199
modules/gdscript/tests/test_completion.h
Normal file
199
modules/gdscript/tests/test_completion.h
Normal file
|
@ -0,0 +1,199 @@
|
|||
/**************************************************************************/
|
||||
/* test_completion.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. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef TEST_COMPLETION_H
|
||||
#define TEST_COMPLETION_H
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
|
||||
#include "core/io/config_file.h"
|
||||
#include "core/io/dir_access.h"
|
||||
#include "core/io/file_access.h"
|
||||
#include "core/object/script_language.h"
|
||||
#include "core/variant/dictionary.h"
|
||||
#include "core/variant/variant.h"
|
||||
#include "gdscript_test_runner.h"
|
||||
#include "modules/modules_enabled.gen.h" // For mono.
|
||||
#include "scene/resources/packed_scene.h"
|
||||
|
||||
#include "../gdscript.h"
|
||||
#include "tests/test_macros.h"
|
||||
|
||||
#include "editor/editor_settings.h"
|
||||
#include "scene/theme/theme_db.h"
|
||||
|
||||
namespace GDScriptTests {
|
||||
|
||||
static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) {
|
||||
if (p_expected.get("display", p_got.display) != p_got.display) {
|
||||
return false;
|
||||
}
|
||||
if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) {
|
||||
return false;
|
||||
}
|
||||
if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) {
|
||||
return false;
|
||||
}
|
||||
if (p_expected.get("location", p_got.location) != Variant(p_got.location)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) {
|
||||
ERR_FAIL_COND(!p_variant.is_array());
|
||||
|
||||
Array arr = p_variant;
|
||||
for (int i = 0; i < arr.size(); i++) {
|
||||
if (arr[i].get_type() == Variant::DICTIONARY) {
|
||||
p_list.push_back(arr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void test_directory(const String &p_dir) {
|
||||
Error err = OK;
|
||||
Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
|
||||
|
||||
if (err != OK) {
|
||||
FAIL("Invalid test directory.");
|
||||
return;
|
||||
}
|
||||
|
||||
String path = 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 = dir->get_next();
|
||||
continue;
|
||||
}
|
||||
test_directory(path.path_join(next));
|
||||
} else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) {
|
||||
Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err);
|
||||
|
||||
if (err != OK) {
|
||||
next = dir->get_next();
|
||||
continue;
|
||||
}
|
||||
|
||||
String code = acc->get_as_utf8_string();
|
||||
// For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.
|
||||
code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF));
|
||||
// Require pointer sentinel char in scripts.
|
||||
CHECK(code.find_char(0xFFFF) != -1);
|
||||
|
||||
ConfigFile conf;
|
||||
if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) {
|
||||
FAIL("No config file found.");
|
||||
}
|
||||
|
||||
#ifndef MODULE_MONO_ENABLED
|
||||
if (conf.get_value("input", "cs", false)) {
|
||||
next = dir->get_next();
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));
|
||||
|
||||
List<Dictionary> include;
|
||||
to_dict_list(conf.get_value("result", "include", Array()), include);
|
||||
|
||||
List<Dictionary> exclude;
|
||||
to_dict_list(conf.get_value("result", "exclude", Array()), exclude);
|
||||
|
||||
List<ScriptLanguage::CodeCompletionOption> options;
|
||||
String call_hint;
|
||||
bool forced;
|
||||
|
||||
Node *owner = nullptr;
|
||||
if (dir->file_exists(next.get_basename() + ".tscn")) {
|
||||
String project_path = "res://completion";
|
||||
Ref<PackedScene> scene = ResourceLoader::load(project_path.path_join(next.get_basename() + ".tscn"), "PackedScene");
|
||||
if (scene.is_valid()) {
|
||||
owner = scene->instantiate();
|
||||
}
|
||||
}
|
||||
|
||||
GDScriptLanguage::get_singleton()->complete_code(code, path.path_join(next), owner, &options, forced, call_hint);
|
||||
String contains_excluded;
|
||||
for (ScriptLanguage::CodeCompletionOption &option : options) {
|
||||
for (const Dictionary &E : exclude) {
|
||||
if (match_option(E, option)) {
|
||||
contains_excluded = option.display;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!contains_excluded.is_empty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const Dictionary &E : include) {
|
||||
if (match_option(E, option)) {
|
||||
include.erase(E);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");
|
||||
CHECK(include.is_empty());
|
||||
|
||||
String expected_call_hint = conf.get_value("result", "call_hint", call_hint);
|
||||
bool expected_forced = conf.get_value("result", "forced", forced);
|
||||
|
||||
CHECK(expected_call_hint == call_hint);
|
||||
CHECK(expected_forced == forced);
|
||||
|
||||
if (owner) {
|
||||
memdelete(owner);
|
||||
}
|
||||
}
|
||||
next = dir->get_next();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SUITE("[Modules][GDScript][Completion]") {
|
||||
TEST_CASE("[Editor] Check suggestion list") {
|
||||
// Set all editor settings that code completion relies on.
|
||||
EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);
|
||||
init_language("modules/gdscript/tests/scripts");
|
||||
|
||||
test_directory("modules/gdscript/tests/scripts/completion");
|
||||
}
|
||||
}
|
||||
} // namespace GDScriptTests
|
||||
|
||||
#endif
|
||||
|
||||
#endif // TEST_COMPLETION_H
|
|
@ -76,7 +76,7 @@ namespace GDScriptTests {
|
|||
// LSP GDScript test scripts are located inside project of other GDScript tests:
|
||||
// Cannot reset `ProjectSettings` (singleton) -> Cannot load another workspace and resources in there.
|
||||
// -> Reuse GDScript test project. LSP specific scripts are then placed inside `lsp` folder.
|
||||
// Access via `res://lsp/my_script.notest.gd`.
|
||||
// Access via `res://lsp/my_script.gd`.
|
||||
const String root = "modules/gdscript/tests/scripts/";
|
||||
|
||||
/*
|
||||
|
@ -394,7 +394,7 @@ func f():
|
|||
Ref<GDScriptWorkspace> workspace = GDScriptLanguageProtocol::get_singleton()->get_workspace();
|
||||
|
||||
{
|
||||
String path = "res://lsp/local_variables.notest.gd";
|
||||
String path = "res://lsp/local_variables.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -413,7 +413,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for indented variables") {
|
||||
String path = "res://lsp/indentation.notest.gd";
|
||||
String path = "res://lsp/indentation.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -421,7 +421,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for scopes") {
|
||||
String path = "res://lsp/scopes.notest.gd";
|
||||
String path = "res://lsp/scopes.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -429,7 +429,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for lambda") {
|
||||
String path = "res://lsp/lambdas.notest.gd";
|
||||
String path = "res://lsp/lambdas.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -437,7 +437,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for inner class") {
|
||||
String path = "res://lsp/class.notest.gd";
|
||||
String path = "res://lsp/class.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -445,7 +445,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for inner class") {
|
||||
String path = "res://lsp/enums.notest.gd";
|
||||
String path = "res://lsp/enums.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -453,7 +453,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for shadowing & shadowed variables") {
|
||||
String path = "res://lsp/shadowing_initializer.notest.gd";
|
||||
String path = "res://lsp/shadowing_initializer.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
@ -461,7 +461,7 @@ func f():
|
|||
}
|
||||
|
||||
SUBCASE("Can get correct ranges for properties and getter/setter") {
|
||||
String path = "res://lsp/properties.notest.gd";
|
||||
String path = "res://lsp/properties.gd";
|
||||
assert_no_errors_in(path);
|
||||
String uri = workspace->get_file_uri(path);
|
||||
Vector<InlineTestData> all_test_data = read_tests(path);
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
|
||||
#include "test_main.h"
|
||||
|
||||
#include "editor/editor_paths.h"
|
||||
#include "editor/editor_settings.h"
|
||||
#include "tests/core/config/test_project_settings.h"
|
||||
#include "tests/core/input/test_input_event.h"
|
||||
#include "tests/core/input/test_input_event_key.h"
|
||||
|
@ -221,7 +223,7 @@ struct GodotTestCaseListener : public doctest::IReporter {
|
|||
String name = String(p_in.m_name);
|
||||
String suite_name = String(p_in.m_test_suite);
|
||||
|
||||
if (name.find("[SceneTree]") != -1) {
|
||||
if (name.find("[SceneTree]") != -1 || name.find("[Editor]") != -1) {
|
||||
memnew(MessageQueue);
|
||||
|
||||
memnew(Input);
|
||||
|
@ -264,6 +266,13 @@ struct GodotTestCaseListener : public doctest::IReporter {
|
|||
if (!DisplayServer::get_singleton()->has_feature(DisplayServer::Feature::FEATURE_SUBWINDOWS)) {
|
||||
SceneTree::get_singleton()->get_root()->set_embedding_subwindows(true);
|
||||
}
|
||||
|
||||
if (name.find("[Editor]") != -1) {
|
||||
Engine::get_singleton()->set_editor_hint(true);
|
||||
EditorPaths::create();
|
||||
EditorSettings::create();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -286,6 +295,12 @@ struct GodotTestCaseListener : public doctest::IReporter {
|
|||
}
|
||||
|
||||
void test_case_end(const doctest::CurrentTestCaseStats &) override {
|
||||
if (EditorSettings::get_singleton()) {
|
||||
EditorSettings::destroy();
|
||||
}
|
||||
|
||||
Engine::get_singleton()->set_editor_hint(false);
|
||||
|
||||
if (SceneTree::get_singleton()) {
|
||||
SceneTree::get_singleton()->finalize();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue