Explorar o código

Add unit test runner for autocompletion

HolonProduction hai 1 ano
pai
achega
af4cbaf751

+ 1 - 1
modules/gdscript/tests/gdscript_test_runner.cpp

@@ -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;
 			}

+ 4 - 0
modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.cfg

@@ -0,0 +1,4 @@
+[output]
+expected=[
+    {"display": "autoplay"},
+]

+ 6 - 0
modules/gdscript/tests/scripts/completion/get_node/get_node_member_annotated.gd

@@ -0,0 +1,6 @@
+extends Node
+
+var test: AnimationPlayer = $AnimationPlayer
+
+func _ready():
+    test.➡

+ 0 - 0
modules/gdscript/tests/scripts/lsp/class.notest.gd → modules/gdscript/tests/scripts/lsp/class.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/enums.notest.gd → modules/gdscript/tests/scripts/lsp/enums.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/indentation.notest.gd → modules/gdscript/tests/scripts/lsp/indentation.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/lambdas.notest.gd → modules/gdscript/tests/scripts/lsp/lambdas.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/local_variables.notest.gd → modules/gdscript/tests/scripts/lsp/local_variables.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/properties.notest.gd → modules/gdscript/tests/scripts/lsp/properties.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/scopes.notest.gd → modules/gdscript/tests/scripts/lsp/scopes.gd


+ 0 - 0
modules/gdscript/tests/scripts/lsp/shadowing_initializer.notest.gd → modules/gdscript/tests/scripts/lsp/shadowing_initializer.gd


+ 1 - 1
modules/gdscript/tests/scripts/project.godot

@@ -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 - 0
modules/gdscript/tests/test_completion.h

@@ -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

+ 9 - 9
modules/gdscript/tests/test_lsp.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);

+ 16 - 1
tests/test_main.cpp

@@ -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();
 		}