test_completion.h 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**************************************************************************/
  2. /* test_completion.h */
  3. /**************************************************************************/
  4. /* This file is part of: */
  5. /* GODOT ENGINE */
  6. /* https://godotengine.org */
  7. /**************************************************************************/
  8. /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
  9. /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
  10. /* */
  11. /* Permission is hereby granted, free of charge, to any person obtaining */
  12. /* a copy of this software and associated documentation files (the */
  13. /* "Software"), to deal in the Software without restriction, including */
  14. /* without limitation the rights to use, copy, modify, merge, publish, */
  15. /* distribute, sublicense, and/or sell copies of the Software, and to */
  16. /* permit persons to whom the Software is furnished to do so, subject to */
  17. /* the following conditions: */
  18. /* */
  19. /* The above copyright notice and this permission notice shall be */
  20. /* included in all copies or substantial portions of the Software. */
  21. /* */
  22. /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
  23. /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
  24. /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
  25. /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
  26. /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
  27. /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
  28. /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
  29. /**************************************************************************/
  30. #pragma once
  31. #ifdef TOOLS_ENABLED
  32. #include "tests/test_macros.h"
  33. #include "../gdscript.h"
  34. #include "gdscript_test_runner.h"
  35. #include "core/config/project_settings.h"
  36. #include "core/io/config_file.h"
  37. #include "core/io/dir_access.h"
  38. #include "core/io/file_access.h"
  39. #include "core/object/script_language.h"
  40. #include "core/variant/dictionary.h"
  41. #include "core/variant/variant.h"
  42. #include "editor/editor_settings.h"
  43. #include "scene/resources/packed_scene.h"
  44. #include "scene/theme/theme_db.h"
  45. #include "modules/modules_enabled.gen.h" // For mono.
  46. namespace GDScriptTests {
  47. static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) {
  48. if (p_expected.get("display", p_got.display) != p_got.display) {
  49. return false;
  50. }
  51. if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) {
  52. return false;
  53. }
  54. if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) {
  55. return false;
  56. }
  57. if (p_expected.get("location", p_got.location) != Variant(p_got.location)) {
  58. return false;
  59. }
  60. return true;
  61. }
  62. static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) {
  63. ERR_FAIL_COND(!p_variant.is_array());
  64. Array arr = p_variant;
  65. for (int i = 0; i < arr.size(); i++) {
  66. if (arr[i].get_type() == Variant::DICTIONARY) {
  67. p_list.push_back(arr[i]);
  68. }
  69. }
  70. }
  71. static void test_directory(const String &p_dir) {
  72. Error err = OK;
  73. Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
  74. if (err != OK) {
  75. FAIL("Invalid test directory.");
  76. return;
  77. }
  78. String path = dir->get_current_dir();
  79. dir->list_dir_begin();
  80. String next = dir->get_next();
  81. while (!next.is_empty()) {
  82. if (dir->current_is_dir()) {
  83. if (next == "." || next == "..") {
  84. next = dir->get_next();
  85. continue;
  86. }
  87. test_directory(path.path_join(next));
  88. } else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) {
  89. Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err);
  90. if (err != OK) {
  91. next = dir->get_next();
  92. continue;
  93. }
  94. String code = acc->get_as_utf8_string();
  95. // For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.
  96. code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF));
  97. // Require pointer sentinel char in scripts.
  98. int location = code.find_char(0xFFFF);
  99. CHECK(location != -1);
  100. String res_path = ProjectSettings::get_singleton()->localize_path(path.path_join(next));
  101. ConfigFile conf;
  102. if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) {
  103. FAIL("No config file found.");
  104. }
  105. #ifndef MODULE_MONO_ENABLED
  106. if (conf.get_value("input", "cs", false)) {
  107. next = dir->get_next();
  108. continue;
  109. }
  110. #endif
  111. EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));
  112. EditorSettings::get_singleton()->set_setting("text_editor/completion/add_node_path_literals", conf.get_value("input", "add_node_path_literals", false));
  113. EditorSettings::get_singleton()->set_setting("text_editor/completion/add_string_name_literals", conf.get_value("input", "add_string_name_literals", false));
  114. List<Dictionary> include;
  115. to_dict_list(conf.get_value("output", "include", Array()), include);
  116. List<Dictionary> exclude;
  117. to_dict_list(conf.get_value("output", "exclude", Array()), exclude);
  118. List<ScriptLanguage::CodeCompletionOption> options;
  119. String call_hint;
  120. bool forced;
  121. Node *scene = nullptr;
  122. if (conf.has_section_key("input", "scene")) {
  123. Ref<PackedScene> packed_scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP);
  124. if (packed_scene.is_valid()) {
  125. scene = packed_scene->instantiate();
  126. }
  127. } else if (dir->file_exists(next.get_basename() + ".tscn")) {
  128. Ref<PackedScene> packed_scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene");
  129. if (packed_scene.is_valid()) {
  130. scene = packed_scene->instantiate();
  131. }
  132. }
  133. Node *owner = nullptr;
  134. if (scene != nullptr) {
  135. owner = scene->get_node(conf.get_value("input", "node_path", "."));
  136. }
  137. // The only requirement is for the script to be parsable, warnings and errors from the analyzer might happen and completion should still work.
  138. ERR_PRINT_OFF;
  139. if (owner != nullptr) {
  140. // Remove the line which contains the sentinel char, to get a valid script.
  141. Ref<GDScript> scr;
  142. scr.instantiate();
  143. int start = location;
  144. int end = location;
  145. for (; start >= 0; --start) {
  146. if (code.get(start) == '\n') {
  147. break;
  148. }
  149. }
  150. for (; end < code.size(); ++end) {
  151. if (code.get(end) == '\n') {
  152. break;
  153. }
  154. }
  155. scr->set_source_code(code.erase(start, end - start));
  156. scr->reload();
  157. scr->set_path(res_path);
  158. owner->set_script(scr);
  159. }
  160. GDScriptLanguage::get_singleton()->complete_code(code, res_path, owner, &options, forced, call_hint);
  161. ERR_PRINT_ON;
  162. String contains_excluded;
  163. for (ScriptLanguage::CodeCompletionOption &option : options) {
  164. for (const Dictionary &E : exclude) {
  165. if (match_option(E, option)) {
  166. contains_excluded = option.display;
  167. break;
  168. }
  169. }
  170. if (!contains_excluded.is_empty()) {
  171. break;
  172. }
  173. for (const Dictionary &E : include) {
  174. if (match_option(E, option)) {
  175. include.erase(E);
  176. break;
  177. }
  178. }
  179. }
  180. CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");
  181. CHECK(include.is_empty());
  182. String expected_call_hint = conf.get_value("output", "call_hint", call_hint);
  183. bool expected_forced = conf.get_value("output", "forced", forced);
  184. CHECK(expected_call_hint == call_hint);
  185. CHECK(expected_forced == forced);
  186. if (scene) {
  187. memdelete(scene);
  188. }
  189. }
  190. next = dir->get_next();
  191. }
  192. }
  193. TEST_SUITE("[Modules][GDScript][Completion]") {
  194. TEST_CASE("[Editor] Check suggestion list") {
  195. // Set all editor settings that code completion relies on.
  196. EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);
  197. init_language("modules/gdscript/tests/scripts");
  198. test_directory("modules/gdscript/tests/scripts/completion");
  199. }
  200. }
  201. } // namespace GDScriptTests
  202. #endif