Path: blob/master/modules/gdscript/tests/test_completion.h
10278 views
/**************************************************************************/1/* test_completion.h */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#pragma once3132#ifdef TOOLS_ENABLED3334#include "tests/test_macros.h"3536#include "../gdscript.h"37#include "gdscript_test_runner.h"3839#include "core/config/project_settings.h"40#include "core/io/config_file.h"41#include "core/io/dir_access.h"42#include "core/io/file_access.h"43#include "core/object/script_language.h"44#include "core/variant/dictionary.h"45#include "core/variant/variant.h"46#include "editor/settings/editor_settings.h"47#include "scene/resources/packed_scene.h"48#include "scene/theme/theme_db.h"4950#include "modules/modules_enabled.gen.h" // For mono.5152namespace GDScriptTests {5354static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) {55if (p_expected.get("display", p_got.display) != p_got.display) {56return false;57}58if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) {59return false;60}61if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) {62return false;63}64if (p_expected.get("location", p_got.location) != Variant(p_got.location)) {65return false;66}67return true;68}6970static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) {71ERR_FAIL_COND(!p_variant.is_array());7273Array arr = p_variant;74for (int i = 0; i < arr.size(); i++) {75if (arr[i].get_type() == Variant::DICTIONARY) {76p_list.push_back(arr[i]);77}78}79}8081static void test_directory(const String &p_dir) {82Error err = OK;83Ref<DirAccess> dir = DirAccess::open(p_dir, &err);8485if (err != OK) {86FAIL("Invalid test directory.");87return;88}8990String path = dir->get_current_dir();9192dir->list_dir_begin();93String next = dir->get_next();9495while (!next.is_empty()) {96if (dir->current_is_dir()) {97if (next == "." || next == "..") {98next = dir->get_next();99continue;100}101test_directory(path.path_join(next));102} else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) {103Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err);104105if (err != OK) {106next = dir->get_next();107continue;108}109110String code = acc->get_as_utf8_string();111// For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.112code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF));113// Require pointer sentinel char in scripts.114int location = code.find_char(0xFFFF);115CHECK(location != -1);116117String res_path = ProjectSettings::get_singleton()->localize_path(path.path_join(next));118119ConfigFile conf;120if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) {121FAIL("No config file found.");122}123124#ifndef MODULE_MONO_ENABLED125if (conf.get_value("input", "cs", false)) {126next = dir->get_next();127continue;128}129#endif130131EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));132EditorSettings::get_singleton()->set_setting("text_editor/completion/add_node_path_literals", conf.get_value("input", "add_node_path_literals", false));133EditorSettings::get_singleton()->set_setting("text_editor/completion/add_string_name_literals", conf.get_value("input", "add_string_name_literals", false));134135List<Dictionary> include;136to_dict_list(conf.get_value("output", "include", Array()), include);137138List<Dictionary> exclude;139to_dict_list(conf.get_value("output", "exclude", Array()), exclude);140141List<ScriptLanguage::CodeCompletionOption> options;142String call_hint;143bool forced;144145Node *scene = nullptr;146if (conf.has_section_key("input", "scene")) {147Ref<PackedScene> packed_scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP);148if (packed_scene.is_valid()) {149scene = packed_scene->instantiate();150}151} else if (dir->file_exists(next.get_basename() + ".tscn")) {152Ref<PackedScene> packed_scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene");153if (packed_scene.is_valid()) {154scene = packed_scene->instantiate();155}156}157Node *owner = nullptr;158if (scene != nullptr) {159owner = scene->get_node(conf.get_value("input", "node_path", "."));160}161162// The only requirement is for the script to be parsable, warnings and errors from the analyzer might happen and completion should still work.163ERR_PRINT_OFF;164if (owner != nullptr) {165// Remove the line which contains the sentinel char, to get a valid script.166Ref<GDScript> scr;167scr.instantiate();168int start = location;169int end = location;170for (; start >= 0; --start) {171if (code.get(start) == '\n') {172break;173}174}175for (; end < code.size(); ++end) {176if (code.get(end) == '\n') {177break;178}179}180scr->set_source_code(code.erase(start, end - start));181scr->reload();182scr->set_path(res_path);183owner->set_script(scr);184}185186GDScriptLanguage::get_singleton()->complete_code(code, res_path, owner, &options, forced, call_hint);187ERR_PRINT_ON;188189String contains_excluded;190for (ScriptLanguage::CodeCompletionOption &option : options) {191for (const Dictionary &E : exclude) {192if (match_option(E, option)) {193contains_excluded = option.display;194break;195}196}197if (!contains_excluded.is_empty()) {198break;199}200201for (const Dictionary &E : include) {202if (match_option(E, option)) {203include.erase(E);204break;205}206}207}208CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");209CHECK(include.is_empty());210211String expected_call_hint = conf.get_value("output", "call_hint", call_hint);212bool expected_forced = conf.get_value("output", "forced", forced);213214CHECK(expected_call_hint == call_hint);215CHECK(expected_forced == forced);216217if (scene) {218memdelete(scene);219}220}221next = dir->get_next();222}223}224225static void setup_global_classes(const String &p_dir) {226Error err = OK;227Ref<DirAccess> dir = DirAccess::open(p_dir, &err);228229if (err != OK) {230FAIL("Invalid test directory.");231return;232}233234String path = dir->get_current_dir();235236dir->list_dir_begin();237String next = dir->get_next();238239while (!next.is_empty()) {240if (dir->current_is_dir() && next != "." && next != "..") {241setup_global_classes(path.path_join(next));242} else if (next.ends_with(".gd")) {243String base_type;244bool is_abstract;245bool is_tool;246String source_file = path.path_join(next);247String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type, nullptr, &is_abstract, &is_tool);248if (class_name.is_empty()) {249next = dir->get_next();250continue;251}252ERR_FAIL_COND_MSG(ScriptServer::is_global_class(class_name),253"Class name \"" + class_name + "\" from \"" + source_file + "\" is already used in \"" + ScriptServer::get_global_class_path(class_name) + "\".");254255ScriptServer::add_global_class(class_name, base_type, GDScriptLanguage::get_singleton()->get_name(), source_file, is_abstract, is_tool);256}257next = dir->get_next();258}259}260261TEST_SUITE("[Modules][GDScript][Completion]") {262TEST_CASE("[Editor] Check suggestion list") {263// Set all editor settings that code completion relies on.264EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);265init_language("modules/gdscript/tests/scripts");266267setup_global_classes("modules/gdscript/tests/scripts/completion");268test_directory("modules/gdscript/tests/scripts/completion");269270finish_language();271}272}273} // namespace GDScriptTests274275#endif276277278