Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/modules/gdscript/tests/test_completion.h
10278 views
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
31
#pragma once
32
33
#ifdef TOOLS_ENABLED
34
35
#include "tests/test_macros.h"
36
37
#include "../gdscript.h"
38
#include "gdscript_test_runner.h"
39
40
#include "core/config/project_settings.h"
41
#include "core/io/config_file.h"
42
#include "core/io/dir_access.h"
43
#include "core/io/file_access.h"
44
#include "core/object/script_language.h"
45
#include "core/variant/dictionary.h"
46
#include "core/variant/variant.h"
47
#include "editor/settings/editor_settings.h"
48
#include "scene/resources/packed_scene.h"
49
#include "scene/theme/theme_db.h"
50
51
#include "modules/modules_enabled.gen.h" // For mono.
52
53
namespace GDScriptTests {
54
55
static bool match_option(const Dictionary p_expected, const ScriptLanguage::CodeCompletionOption p_got) {
56
if (p_expected.get("display", p_got.display) != p_got.display) {
57
return false;
58
}
59
if (p_expected.get("insert_text", p_got.insert_text) != p_got.insert_text) {
60
return false;
61
}
62
if (p_expected.get("kind", p_got.kind) != Variant(p_got.kind)) {
63
return false;
64
}
65
if (p_expected.get("location", p_got.location) != Variant(p_got.location)) {
66
return false;
67
}
68
return true;
69
}
70
71
static void to_dict_list(Variant p_variant, List<Dictionary> &p_list) {
72
ERR_FAIL_COND(!p_variant.is_array());
73
74
Array arr = p_variant;
75
for (int i = 0; i < arr.size(); i++) {
76
if (arr[i].get_type() == Variant::DICTIONARY) {
77
p_list.push_back(arr[i]);
78
}
79
}
80
}
81
82
static void test_directory(const String &p_dir) {
83
Error err = OK;
84
Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
85
86
if (err != OK) {
87
FAIL("Invalid test directory.");
88
return;
89
}
90
91
String path = dir->get_current_dir();
92
93
dir->list_dir_begin();
94
String next = dir->get_next();
95
96
while (!next.is_empty()) {
97
if (dir->current_is_dir()) {
98
if (next == "." || next == "..") {
99
next = dir->get_next();
100
continue;
101
}
102
test_directory(path.path_join(next));
103
} else if (next.ends_with(".gd") && !next.ends_with(".notest.gd")) {
104
Ref<FileAccess> acc = FileAccess::open(path.path_join(next), FileAccess::READ, &err);
105
106
if (err != OK) {
107
next = dir->get_next();
108
continue;
109
}
110
111
String code = acc->get_as_utf8_string();
112
// For ease of reading ➡ (0x27A1) acts as sentinel char instead of 0xFFFF in the files.
113
code = code.replace_first(String::chr(0x27A1), String::chr(0xFFFF));
114
// Require pointer sentinel char in scripts.
115
int location = code.find_char(0xFFFF);
116
CHECK(location != -1);
117
118
String res_path = ProjectSettings::get_singleton()->localize_path(path.path_join(next));
119
120
ConfigFile conf;
121
if (conf.load(path.path_join(next.get_basename() + ".cfg")) != OK) {
122
FAIL("No config file found.");
123
}
124
125
#ifndef MODULE_MONO_ENABLED
126
if (conf.get_value("input", "cs", false)) {
127
next = dir->get_next();
128
continue;
129
}
130
#endif
131
132
EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", conf.get_value("input", "use_single_quotes", false));
133
EditorSettings::get_singleton()->set_setting("text_editor/completion/add_node_path_literals", conf.get_value("input", "add_node_path_literals", false));
134
EditorSettings::get_singleton()->set_setting("text_editor/completion/add_string_name_literals", conf.get_value("input", "add_string_name_literals", false));
135
136
List<Dictionary> include;
137
to_dict_list(conf.get_value("output", "include", Array()), include);
138
139
List<Dictionary> exclude;
140
to_dict_list(conf.get_value("output", "exclude", Array()), exclude);
141
142
List<ScriptLanguage::CodeCompletionOption> options;
143
String call_hint;
144
bool forced;
145
146
Node *scene = nullptr;
147
if (conf.has_section_key("input", "scene")) {
148
Ref<PackedScene> packed_scene = ResourceLoader::load(conf.get_value("input", "scene"), "PackedScene", ResourceFormatLoader::CACHE_MODE_IGNORE_DEEP);
149
if (packed_scene.is_valid()) {
150
scene = packed_scene->instantiate();
151
}
152
} else if (dir->file_exists(next.get_basename() + ".tscn")) {
153
Ref<PackedScene> packed_scene = ResourceLoader::load(path.path_join(next.get_basename() + ".tscn"), "PackedScene");
154
if (packed_scene.is_valid()) {
155
scene = packed_scene->instantiate();
156
}
157
}
158
Node *owner = nullptr;
159
if (scene != nullptr) {
160
owner = scene->get_node(conf.get_value("input", "node_path", "."));
161
}
162
163
// The only requirement is for the script to be parsable, warnings and errors from the analyzer might happen and completion should still work.
164
ERR_PRINT_OFF;
165
if (owner != nullptr) {
166
// Remove the line which contains the sentinel char, to get a valid script.
167
Ref<GDScript> scr;
168
scr.instantiate();
169
int start = location;
170
int end = location;
171
for (; start >= 0; --start) {
172
if (code.get(start) == '\n') {
173
break;
174
}
175
}
176
for (; end < code.size(); ++end) {
177
if (code.get(end) == '\n') {
178
break;
179
}
180
}
181
scr->set_source_code(code.erase(start, end - start));
182
scr->reload();
183
scr->set_path(res_path);
184
owner->set_script(scr);
185
}
186
187
GDScriptLanguage::get_singleton()->complete_code(code, res_path, owner, &options, forced, call_hint);
188
ERR_PRINT_ON;
189
190
String contains_excluded;
191
for (ScriptLanguage::CodeCompletionOption &option : options) {
192
for (const Dictionary &E : exclude) {
193
if (match_option(E, option)) {
194
contains_excluded = option.display;
195
break;
196
}
197
}
198
if (!contains_excluded.is_empty()) {
199
break;
200
}
201
202
for (const Dictionary &E : include) {
203
if (match_option(E, option)) {
204
include.erase(E);
205
break;
206
}
207
}
208
}
209
CHECK_MESSAGE(contains_excluded.is_empty(), "Autocompletion suggests illegal option '", contains_excluded, "' for '", path.path_join(next), "'.");
210
CHECK(include.is_empty());
211
212
String expected_call_hint = conf.get_value("output", "call_hint", call_hint);
213
bool expected_forced = conf.get_value("output", "forced", forced);
214
215
CHECK(expected_call_hint == call_hint);
216
CHECK(expected_forced == forced);
217
218
if (scene) {
219
memdelete(scene);
220
}
221
}
222
next = dir->get_next();
223
}
224
}
225
226
static void setup_global_classes(const String &p_dir) {
227
Error err = OK;
228
Ref<DirAccess> dir = DirAccess::open(p_dir, &err);
229
230
if (err != OK) {
231
FAIL("Invalid test directory.");
232
return;
233
}
234
235
String path = dir->get_current_dir();
236
237
dir->list_dir_begin();
238
String next = dir->get_next();
239
240
while (!next.is_empty()) {
241
if (dir->current_is_dir() && next != "." && next != "..") {
242
setup_global_classes(path.path_join(next));
243
} else if (next.ends_with(".gd")) {
244
String base_type;
245
bool is_abstract;
246
bool is_tool;
247
String source_file = path.path_join(next);
248
String class_name = GDScriptLanguage::get_singleton()->get_global_class_name(source_file, &base_type, nullptr, &is_abstract, &is_tool);
249
if (class_name.is_empty()) {
250
next = dir->get_next();
251
continue;
252
}
253
ERR_FAIL_COND_MSG(ScriptServer::is_global_class(class_name),
254
"Class name \"" + class_name + "\" from \"" + source_file + "\" is already used in \"" + ScriptServer::get_global_class_path(class_name) + "\".");
255
256
ScriptServer::add_global_class(class_name, base_type, GDScriptLanguage::get_singleton()->get_name(), source_file, is_abstract, is_tool);
257
}
258
next = dir->get_next();
259
}
260
}
261
262
TEST_SUITE("[Modules][GDScript][Completion]") {
263
TEST_CASE("[Editor] Check suggestion list") {
264
// Set all editor settings that code completion relies on.
265
EditorSettings::get_singleton()->set_setting("text_editor/completion/use_single_quotes", false);
266
init_language("modules/gdscript/tests/scripts");
267
268
setup_global_classes("modules/gdscript/tests/scripts/completion");
269
test_directory("modules/gdscript/tests/scripts/completion");
270
271
finish_language();
272
}
273
}
274
} // namespace GDScriptTests
275
276
#endif
277
278