3 from __future__
import absolute_import
, division
, print_function
, unicode_literals
11 from typing
import Iterable
, List
, Mapping
, Tuple
14 from hh_paths
import hh_server
15 from lspcommand
import LspCommandProcessor
, Transcript
16 from lsptestspec
import LspTestSpec
17 from test_case
import TestCase
18 from utils
import Json
, JsonObject
, interpolate_variables
21 class LspTestDriver(common_tests
.CommonTestDriver
):
22 def write_load_config(self
, use_saved_state
: bool = False) -> None:
23 # Will use the .hhconfig already in the repo directory
24 # As for hh.conf, we'll write it explicitly each test.
25 # Note that hh.conf uses lower-case...
26 use_saved_state_str
= "true" if use_saved_state
else "false"
27 with
open(os
.path
.join(self
.repo_dir
, "hh.conf"), "w") as f
:
31 watchman_subscribe_v2 = true
32 interrupt_on_watchman = true
33 interrupt_on_client = true
35 load_state_natively_v4 = {use_saved_state}
36 use_mini_state = {use_saved_state}
37 require_mini_state = {use_saved_state}
38 lazy_decl = {use_saved_state}
39 lazy_parse = {use_saved_state}
40 lazy_init2 = {use_saved_state}
42 use_saved_state
=use_saved_state_str
46 def write_naming_table_saved_state(self
) -> str:
47 naming_table_saved_state_path
= os
.path
.join(
48 self
.repo_dir
, "naming_table_saved_state.sqlite"
50 (stdout
, stderr
, retcode
) = self
.proc_call(
56 naming_table_saved_state_path
,
59 assert retcode
== 0, (
60 f
"Failed to save naming table saved state: {retcode}\n"
61 + f
"STDOUT:\n{stdout}\n"
62 + f
"STDERR:\n{stderr}\n"
64 return naming_table_saved_state_path
67 class TestLsp(TestCase
[LspTestDriver
]):
69 def get_test_driver(cls
) -> LspTestDriver
:
70 return LspTestDriver()
73 def get_template_repo(cls
) -> str:
74 return "hphp/hack/test/integration/data/lsp_exchanges/"
76 def repo_file(self
, file: str) -> str:
77 return os
.path
.join(self
.test_driver
.repo_dir
, file)
79 def read_repo_file(self
, file: str) -> str:
80 with
open(self
.repo_file(file), "r") as f
:
83 def repo_file_uri(self
, file: str) -> str:
84 return urllib
.parse
.urljoin("file://", self
.repo_file(file))
86 def parse_test_data(self
, file: str, variables
: Mapping
[str, str]) -> Json
:
87 text
= self
.read_repo_file(file)
88 data
: Json
= json
.loads(text
)
89 data
= interpolate_variables(data
, variables
)
93 self
, test_name
: str, variables
: Mapping
[str, str]
94 ) -> Tuple
[Json
, Json
]:
95 test
= self
.parse_test_data(test_name
+ ".json", variables
)
96 expected
= self
.parse_test_data(test_name
+ ".expected", variables
)
97 return (test
, expected
)
99 def write_observed(self
, test_name
: str, observed_transcript
: Json
) -> None:
100 file = os
.path
.join(self
.test_driver
.template_repo
, test_name
+ ".observed.log")
102 list(self
.get_important_received_items(observed_transcript
)), indent
=2
104 with
open(file, "w") as f
:
107 def order_response(self
, response
: JsonObject
) -> str:
109 return str(response
["id"])
111 return json
.dumps(response
, indent
=2)
113 # sorts a list of responses using the 'id' parameter so they can be
114 # compared in sequence even if they came back from the server out of sequence.
115 # this can happen based on how json rpc is specified to work.
116 # if 'id' isn't present the response is a notification. we sort notifications
117 # by their entire text.
118 def sort_responses(self
, responses
: Iterable
[JsonObject
]) -> List
[JsonObject
]:
119 return sorted(responses
, key
=lambda response
: self
.order_response(response
))
121 # removes stack traces from error responses since these can be noisy
122 # as code changes and they contain execution environment specific details
123 # by ignoring these when comparing responses we might miss some minor issues
124 # but will still catch the core error being thrown or not.
125 def sanitize_exceptions(
126 self
, responses
: Iterable
[JsonObject
]
127 ) -> Iterable
[JsonObject
]:
128 sanitized
= copy
.deepcopy(responses
)
129 for response
in sanitized
:
130 if "error" in response
:
131 if "data" in response
["error"]:
132 if "stack" in response
["error"]["data"]:
133 del response
["error"]["data"]["stack"]
136 # dumps an LSP response into a standard json format that can be used for
137 # doing precise text comparison in a way that is human readable in the case
138 # of there being an error.
139 def serialize_responses(self
, responses
: Iterable
[Json
]) -> List
[str]:
140 return [json
.dumps(response
, indent
=2) for response
in responses
]
142 # generates received responses from an LSP communication transcript
143 # ignoring the non-deterministic ones "progress" and "actionRequired"
144 def get_important_received_items(self
, transcript
: Transcript
) -> Iterable
[Json
]:
145 for entry
in transcript
.values():
146 received
= entry
.received
or None
149 method
= received
.get("method") or ""
152 "window/actionRequired",
159 # gets a set of loaded responses ready for validation by sorting them
160 # by id and serializing them for precise text comparison
161 def prepare_responses(self
, responses
: Iterable
[JsonObject
]) -> List
[str]:
162 return self
.serialize_responses(
163 self
.sanitize_exceptions(self
.sort_responses(responses
))
171 wait_for_server
: bool,
172 use_serverless_ide
: bool,
175 assert not use_serverless_ide
, (
176 "Warning: both `wait_for_server` and `use_serverless_ide` "
177 + "were set to `True` for testing in "
178 + self
.run_lsp_test
.__name
__
180 + "While this is a possible test case, it hasn't been written yet, "
181 + "so it's more likely that this is a mistake "
182 + "and you're accidentally relying on hh_server to fulfill "
183 + "serverless IDE requests."
184 + "(If you're writing that test, "
185 + "then it's time to remove this assertion.)"
188 # wait until hh_server is ready before starting lsp
189 self
.test_driver
.run_check()
190 elif use_serverless_ide
:
191 self
.test_driver
.stop_hh_server()
193 with LspCommandProcessor
.create(
194 self
.test_driver
.test_env
, use_serverless_ide
=use_serverless_ide
196 observed_transcript
= lsp
.communicate(test
)
198 self
.write_observed(test_name
, observed_transcript
)
200 expected_items
= self
.prepare_responses(expected
)
201 observed_items
= self
.prepare_responses(
202 list(self
.get_important_received_items(observed_transcript
))
205 if not use_serverless_ide
:
206 # If the server's busy, maybe the machine's just under too much
207 # pressure to give results in a timely fashion. Doing a retry would
208 # only defer the question of what to do in that case, so instead
210 self
.throw_on_skip(observed_transcript
)
212 # validation checks that the number of items matches and that
213 # the responses are exactly identical to what we expect
217 "Wrong count. Observed this:\n"
218 + json
.dumps(observed_transcript
, indent
=2, separators
=(",", ": ")),
220 for i
in range(len(expected_items
)):
221 self
.assertEqual(expected_items
[i
], observed_items
[i
])
223 def throw_on_skip(self
, transcript
: Transcript
) -> None:
224 failure_messages
= ["Server busy", "timed out"]
225 for entry
in transcript
.values():
226 received
= entry
.received
229 if received
.get("error"):
230 message
= received
["error"]["message"]
231 for failure_message
in failure_messages
:
232 if failure_message
in message
:
233 raise unittest
.SkipTest(message
)
235 def prepare_server_environment(self
) -> None:
237 self
.test_driver
.write_load_config()
238 self
.test_driver
.start_hh_server()
239 (output
, err
, _
) = self
.test_driver
.run_check()
240 if "Error: Ran out of retries" in err
:
241 raise unittest
.SkipTest("Hack server could not be launched")
242 self
.assertEqual(output
.strip(), "No errors!")
244 def prepare_serverless_ide_environment(self
) -> Mapping
[str, str]:
246 self
.test_driver
.write_load_config(use_saved_state
=False)
247 naming_table_saved_state_path
= (
248 self
.test_driver
.write_naming_table_saved_state()
250 return {"naming_table_saved_state_path": naming_table_saved_state_path
}
255 variables
: Mapping
[str, str],
256 wait_for_server
: bool = True,
257 use_serverless_ide
: bool = False,
259 test
, expected
= self
.load_test_data(test_name
, variables
)
264 wait_for_server
=wait_for_server
,
265 use_serverless_ide
=use_serverless_ide
,
271 variables
: Mapping
[str, str],
272 wait_for_server
: bool,
273 use_serverless_ide
: bool,
276 assert not use_serverless_ide
, (
277 "Warning: both `wait_for_server` and `use_serverless_ide` "
278 + "were set to `True` for testing in "
279 + self
.run_lsp_test
.__name
__
281 + "While this is a possible test case, it hasn't been written yet, "
282 + "so it's more likely that this is a mistake "
283 + "and you're accidentally relying on hh_server to fulfill "
284 + "serverless IDE requests."
285 + "(If you're writing that test, "
286 + "then it's time to remove this assertion.)"
289 # wait until hh_server is ready before starting lsp
290 self
.test_driver
.run_check()
291 elif use_serverless_ide
:
292 self
.test_driver
.stop_hh_server()
294 with LspCommandProcessor
.create(
295 self
.test_driver
.test_env
, use_serverless_ide
=use_serverless_ide
296 ) as lsp_command_processor
:
297 (observed_transcript
, error_details
) = spec
.run(
298 lsp_command_processor
=lsp_command_processor
, variables
=variables
300 file = os
.path
.join(self
.test_driver
.template_repo
, spec
.name
+ ".sent.log")
304 for sent
, _received
in observed_transcript
.values()
309 with
open(file, "w") as f
:
312 file = os
.path
.join(self
.test_driver
.template_repo
, spec
.name
+ ".received.log")
316 for _sent
, received
in observed_transcript
.values()
317 if received
is not None
321 with
open(file, "w") as f
:
324 if not use_serverless_ide
:
325 # If the server's busy, maybe the machine's just under too much
326 # pressure to give results in a timely fashion. Doing a retry would
327 # only defer the question of what to do in that case, so instead
329 self
.throw_on_skip(observed_transcript
)
331 if error_details
is not None:
332 raise AssertionError(error_details
)
334 def setup_php_file(self
, test_php
: str) -> Mapping
[str, str]:
335 # We want the path to the builtins directory. This is best we can do.
336 (output
, err
, retcode
) = self
.test_driver
.run_check(
337 options
=["--identify-function", "2:21", "--json"],
338 stdin
="<?hh // partial\nfunction f():void {PHP_EOL;}\n",
342 "Could not discover builtins directory -- "
343 + "got exit code 7 (either Out_of_time or Out_of_retries). "
344 + "The test machine is likely under too much load."
346 self
.assertEqual(retcode
, 0)
347 constants_path
= json
.loads(output
)[0]["definition_pos"]["filename"]
349 "hhi_path": re
.sub("/constants.hhi$", "", constants_path
),
350 "root_path": self
.test_driver
.repo_dir
,
351 "php_file_uri": self
.repo_file_uri(test_php
),
352 "php_file": self
.read_repo_file(test_php
),
355 def test_init_shutdown(self
) -> None:
356 self
.prepare_server_environment()
359 "initialize_shutdown", {"root_path": self
.test_driver
.repo_dir
}
362 def test_completion(self
) -> None:
363 self
.prepare_server_environment()
364 variables
= self
.setup_php_file("completion.php")
365 self
.load_and_run("completion", variables
)
367 def test_completion_legacy(self
) -> None:
368 self
.prepare_server_environment()
369 variables
= self
.setup_php_file("completion.php")
370 self
.load_and_run("completion_legacy", variables
)
372 def test_serverless_ide_definition(self
) -> None:
373 variables
= dict(self
.prepare_serverless_ide_environment())
374 variables
.update(self
.setup_php_file("definition.php"))
375 self
.test_driver
.stop_hh_server()
378 self
.initialize_spec(
379 LspTestSpec("serverless_ide_definition"), use_serverless_ide
=True
382 method
="textDocument/didOpen",
385 "uri": "${php_file_uri}",
386 "languageId": "hack",
388 "text": "${php_file}",
393 comment
="call to `b_definition`",
394 method
="textDocument/definition",
396 "textDocument": {"uri": "${php_file_uri}"},
397 "position": {"line": 3, "character": 10},
401 "uri": "file://${root_path}/definition.php",
403 "start": {"line": 6, "character": 9},
404 "end": {"line": 6, "character": 21},
406 "title": "b_definition",
409 powered_by
="serverless_ide",
412 comment
="call to `new BB(1)`",
413 method
="textDocument/definition",
415 "textDocument": {"uri": "${php_file_uri}"},
416 "position": {"line": 29, "character": 13},
420 "uri": "file://${root_path}/definition.php",
422 "start": {"line": 11, "character": 18},
423 "end": {"line": 11, "character": 29},
425 "title": "BB::__construct",
428 powered_by
="serverless_ide",
431 comment
="call to `new CC(1)`",
432 method
="textDocument/definition",
434 "textDocument": {"uri": "${php_file_uri}"},
435 "position": {"line": 30, "character": 13},
439 "uri": "file://${root_path}/definition.php",
441 "start": {"line": 14, "character": 6},
442 "end": {"line": 14, "character": 8},
447 "uri": "file://${root_path}/definition.php",
449 "start": {"line": 11, "character": 18},
450 "end": {"line": 11, "character": 29},
452 "title": "BB::__construct",
455 powered_by
="serverless_ide",
458 comment
="call to `new DD(1)`",
459 method
="textDocument/definition",
461 "textDocument": {"uri": "${php_file_uri}"},
462 "position": {"line": 31, "character": 13},
466 "uri": "file://${root_path}/definition.php",
468 "start": {"line": 17, "character": 6},
469 "end": {"line": 17, "character": 8},
474 "uri": "file://${root_path}/definition.php",
476 "start": {"line": 11, "character": 18},
477 "end": {"line": 11, "character": 29},
479 "title": "BB::__construct",
482 powered_by
="serverless_ide",
485 comment
="call to `new EE(1)`",
486 method
="textDocument/definition",
488 "textDocument": {"uri": "${php_file_uri}"},
489 "position": {"line": 32, "character": 13},
493 "uri": "file://${root_path}/definition.php",
495 "start": {"line": 21, "character": 18},
496 "end": {"line": 21, "character": 29},
498 "title": "EE::__construct",
501 powered_by
="serverless_ide",
504 comment
="call to `new FF(1)`",
505 method
="textDocument/definition",
507 "textDocument": {"uri": "${php_file_uri}"},
508 "position": {"line": 33, "character": 13},
512 "uri": "file://${root_path}/definition.php",
514 "start": {"line": 26, "character": 6},
515 "end": {"line": 26, "character": 8},
520 powered_by
="serverless_ide",
523 comment
="call to `new TakesString(HasString::MyString)`",
524 method
="textDocument/definition",
526 "textDocument": {"uri": "${php_file_uri}"},
527 "position": {"line": 45, "character": 23},
531 "uri": "file://${root_path}/definition.php",
533 "start": {"line": 40, "character": 6},
534 "end": {"line": 40, "character": 15},
536 "title": "HasString",
539 powered_by
="serverless_ide",
542 comment
="make local, unsaved change to the file",
543 method
="textDocument/didChange",
545 "textDocument": {"uri": "${php_file_uri}", "version": 2},
550 "start": {"line": 3, "character": 9},
551 "end": {"line": 3, "character": 21},
558 comment
="call to `test` instead of `b_definition`",
559 method
="textDocument/definition",
561 "textDocument": {"uri": "${php_file_uri}"},
562 "position": {"line": 3, "character": 10},
566 "uri": "file://${root_path}/definition.php",
568 "start": {"line": 28, "character": 9},
569 "end": {"line": 28, "character": 13},
574 powered_by
="serverless_ide",
576 .request(method
="shutdown", params
={}, result
=None)
578 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
580 def test_serverless_ide_document_symbol(self
) -> None:
581 variables
= dict(self
.prepare_serverless_ide_environment())
582 variables
.update(self
.setup_php_file("definition.php"))
583 self
.test_driver
.stop_hh_server()
586 self
.initialize_spec(
587 LspTestSpec("serverless_ide_document_symbol"), use_serverless_ide
=True
590 method
="textDocument/didOpen",
593 "uri": "${php_file_uri}",
594 "languageId": "hack",
596 "text": "${php_file}",
601 comment
="documentSymbol call",
602 method
="textDocument/documentSymbol",
603 params
={"textDocument": {"uri": "${php_file_uri}"}},
606 "name": "testClassMemberInsideConstructorInvocation",
609 "uri": "file://${root_path}/definition.php",
611 "start": {"line": 44, "character": 0},
612 "end": {"line": 46, "character": 1},
620 "uri": "file://${root_path}/definition.php",
622 "start": {"line": 41, "character": 8},
623 "end": {"line": 41, "character": 29},
626 "containerName": "HasString",
632 "uri": "file://${root_path}/definition.php",
634 "start": {"line": 40, "character": 0},
635 "end": {"line": 42, "character": 1},
640 "name": "__construct",
643 "uri": "file://${root_path}/definition.php",
645 "start": {"line": 37, "character": 2},
646 "end": {"line": 37, "character": 43},
649 "containerName": "TakesString",
652 "name": "TakesString",
655 "uri": "file://${root_path}/definition.php",
657 "start": {"line": 36, "character": 0},
658 "end": {"line": 38, "character": 1},
666 "uri": "file://${root_path}/definition.php",
668 "start": {"line": 26, "character": 0},
669 "end": {"line": 26, "character": 11},
674 "name": "__construct",
677 "uri": "file://${root_path}/definition.php",
679 "start": {"line": 21, "character": 2},
680 "end": {"line": 23, "character": 3},
683 "containerName": "EE",
689 "uri": "file://${root_path}/definition.php",
691 "start": {"line": 20, "character": 0},
692 "end": {"line": 24, "character": 1},
700 "uri": "file://${root_path}/definition.php",
702 "start": {"line": 14, "character": 0},
703 "end": {"line": 15, "character": 1},
708 "name": "__construct",
711 "uri": "file://${root_path}/definition.php",
713 "start": {"line": 11, "character": 2},
714 "end": {"line": 11, "character": 40},
717 "containerName": "BB",
723 "uri": "file://${root_path}/definition.php",
725 "start": {"line": 10, "character": 0},
726 "end": {"line": 12, "character": 1},
731 "name": "a_definition",
734 "uri": "file://${root_path}/definition.php",
736 "start": {"line": 2, "character": 0},
737 "end": {"line": 4, "character": 1},
742 "name": "b_definition",
745 "uri": "file://${root_path}/definition.php",
747 "start": {"line": 6, "character": 0},
748 "end": {"line": 8, "character": 1},
756 "uri": "file://${root_path}/definition.php",
758 "start": {"line": 17, "character": 0},
759 "end": {"line": 18, "character": 1},
767 "uri": "file://${root_path}/definition.php",
769 "start": {"line": 28, "character": 0},
770 "end": {"line": 34, "character": 1},
775 powered_by
="serverless_ide",
777 .request(method
="shutdown", params
={}, result
=None)
779 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
782 self
, spec
: LspTestSpec
, use_serverless_ide
: bool
784 if use_serverless_ide
:
785 initialization_options
= {
786 "namingTableSavedStatePath": "${naming_table_saved_state_path}"
789 initialization_options
= {}
791 spec
= spec
.ignore_notifications(method
="telemetry/event").request(
794 "initializationOptions": initialization_options
,
796 "rootPath": "${root_path}",
801 "textDocumentSync": {
805 "willSaveWaitUntil": False,
806 "save": {"includeText": False},
808 "hoverProvider": True,
809 "completionProvider": {
810 "resolveProvider": True,
811 "triggerCharacters": ["$", ">", "\\", ":", "<", "[", "'", '"'],
813 "signatureHelpProvider": {"triggerCharacters": ["(", ","]},
814 "definitionProvider": True,
815 "typeDefinitionProvider": True,
816 "referencesProvider": True,
817 "documentHighlightProvider": True,
818 "documentSymbolProvider": True,
819 "workspaceSymbolProvider": True,
820 "codeActionProvider": False,
821 "documentFormattingProvider": True,
822 "documentRangeFormattingProvider": True,
823 "documentOnTypeFormattingProvider": {
824 "firstTriggerCharacter": ";",
825 "moreTriggerCharacter": ["}"],
827 "renameProvider": True,
828 "typeCoverageProvider": True,
829 "rageProvider": True,
833 if use_serverless_ide
:
834 spec
= spec
.wait_for_server_request(
835 method
="client/registerCapability",
839 "id": "did-change-watched-files",
840 "method": "workspace/didChangeWatchedFiles",
842 "watchers": [{"globPattern": "**", "kind": 7}]
851 def test_serverless_ide_type_definition(self
) -> None:
852 variables
= dict(self
.prepare_serverless_ide_environment())
853 variables
.update(self
.setup_php_file("type_definition.php"))
854 self
.test_driver
.stop_hh_server()
857 self
.initialize_spec(
858 LspTestSpec("serverless_ide_type_definition"), use_serverless_ide
=True
861 method
="textDocument/didOpen",
864 "uri": "${php_file_uri}",
865 "languageId": "hack",
867 "text": "${php_file}",
872 comment
="Conditional Type Definition of HH or II",
873 method
="textDocument/typeDefinition",
875 "textDocument": {"uri": "${php_file_uri}"},
876 "position": {"line": 32, "character": 2},
880 "uri": "${php_file_uri}",
882 "start": {"line": 2, "character": 6},
883 "end": {"line": 2, "character": 8},
888 "uri": "${php_file_uri}",
890 "start": {"line": 12, "character": 6},
891 "end": {"line": 12, "character": 8},
896 powered_by
="serverless_ide",
899 comment
="Standard Class Definition",
900 method
="textDocument/typeDefinition",
902 "textDocument": {"uri": "${php_file_uri}"},
903 "position": {"line": 40, "character": 2},
907 "uri": "${php_file_uri}",
909 "start": {"line": 2, "character": 6},
910 "end": {"line": 2, "character": 8},
915 powered_by
="serverless_ide",
918 comment
="Class Type Definition with Casting",
919 method
="textDocument/typeDefinition",
921 "textDocument": {"uri": "${php_file_uri}"},
922 "position": {"line": 41, "character": 2},
926 "uri": "${php_file_uri}",
928 "start": {"line": 2, "character": 6},
929 "end": {"line": 2, "character": 8},
934 powered_by
="serverless_ide",
937 comment
="Primitive Type Definition",
938 method
="textDocument/typeDefinition",
940 "textDocument": {"uri": "${php_file_uri}"},
941 "position": {"line": 42, "character": 2},
944 powered_by
="serverless_ide",
947 comment
="Function Return Type Definition",
948 method
="textDocument/typeDefinition",
950 "textDocument": {"uri": "${php_file_uri}"},
951 "position": {"line": 43, "character": 2},
955 "uri": "${php_file_uri}",
957 "start": {"line": 12, "character": 6},
958 "end": {"line": 12, "character": 8},
963 powered_by
="serverless_ide",
966 comment
="Function definition with primitive return type",
967 method
="textDocument/typeDefinition",
969 "textDocument": {"uri": "${php_file_uri}"},
970 "position": {"line": 44, "character": 2},
974 "uri": "${php_file_uri}",
976 "start": {"line": 22, "character": 9},
977 "end": {"line": 22, "character": 29},
979 "title": "(function(): int)",
982 powered_by
="serverless_ide",
984 .request(method
="shutdown", params
={}, result
=None)
986 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
988 def test_serverless_ide_hover(self
) -> None:
989 variables
= dict(self
.prepare_serverless_ide_environment())
990 variables
.update(self
.setup_php_file("hover.php"))
991 self
.test_driver
.stop_hh_server()
994 self
.initialize_spec(
995 LspTestSpec("serverless_ide_hover"), use_serverless_ide
=True
998 method
="textDocument/didOpen",
1001 "uri": "${php_file_uri}",
1002 "languageId": "hack",
1004 "text": "${php_file}",
1009 comment
="hover over function invocation",
1010 method
="textDocument/hover",
1012 "textDocument": {"uri": "${php_file_uri}"},
1013 "position": {"line": 3, "character": 16},
1017 {"language": "hack", "value": "int"},
1018 "A comment describing b_hover.",
1021 "start": {"line": 3, "character": 9},
1022 "end": {"line": 3, "character": 16},
1025 powered_by
="serverless_ide",
1028 comment
="hover over whitespace",
1029 method
="textDocument/hover",
1031 "textDocument": {"uri": "${php_file_uri}"},
1032 "position": {"line": 3, "character": 1},
1035 powered_by
="serverless_ide",
1038 comment
="hover over a keyword",
1039 method
="textDocument/hover",
1041 "textDocument": {"uri": "${php_file_uri}"},
1042 "position": {"line": 2, "character": 1},
1045 powered_by
="serverless_ide",
1048 comment
="hover over a comment",
1049 method
="textDocument/hover",
1051 "textDocument": {"uri": "${php_file_uri}"},
1052 "position": {"line": 1, "character": 4},
1055 powered_by
="serverless_ide",
1058 comment
="hover past the end of a line",
1059 method
="textDocument/hover",
1061 "textDocument": {"uri": "${php_file_uri}"},
1062 "position": {"line": 3, "character": 100},
1065 powered_by
="serverless_ide",
1068 comment
="hover past the end of a file",
1069 method
="textDocument/hover",
1071 "textDocument": {"uri": "${php_file_uri}"},
1072 "position": {"line": 300, "character": 0},
1075 powered_by
="serverless_ide",
1077 .request(method
="shutdown", params
={}, result
=None)
1079 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1081 def test_serverless_ide_file_touched_on_disk(self
) -> None:
1082 variables
= dict(self
.prepare_serverless_ide_environment())
1083 variables
.update(self
.setup_php_file("hover.php"))
1084 self
.test_driver
.stop_hh_server()
1087 self
.initialize_spec(
1088 LspTestSpec("serverless_ide_file_on_disk_change"),
1089 use_serverless_ide
=True,
1092 method
="textDocument/didOpen",
1095 "uri": "${php_file_uri}",
1096 "languageId": "hack",
1098 "text": "${php_file}",
1103 method
="workspace/didChangeWatchedFiles",
1104 params
={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
1106 .wait_for_notification(
1107 comment
="wait for sIDE to process file change",
1108 method
="telemetry/event",
1111 "message": "[client-ide] Done processing file changes",
1115 method
="textDocument/hover",
1117 "textDocument": {"uri": "${php_file_uri}"},
1118 "position": {"line": 3, "character": 16},
1122 {"language": "hack", "value": "int"},
1123 "A comment describing b_hover.",
1126 "start": {"line": 3, "character": 9},
1127 "end": {"line": 3, "character": 16},
1130 powered_by
="serverless_ide",
1132 .request(method
="shutdown", params
={}, result
=None)
1134 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1136 def test_serverless_ide_file_hover_with_errors(self
) -> None:
1137 variables
= dict(self
.prepare_serverless_ide_environment())
1138 variables
.update(self
.setup_php_file("hover_with_errors.php"))
1139 self
.test_driver
.stop_hh_server()
1142 self
.initialize_spec(
1143 LspTestSpec("serverless_ide_hover_with_errors"), use_serverless_ide
=True
1146 method
="textDocument/didOpen",
1149 "uri": "${php_file_uri}",
1150 "languageId": "hack",
1152 "text": "${php_file}",
1157 method
="workspace/didChangeWatchedFiles",
1158 params
={"changes": [{"uri": "${php_file_uri}", "type": 2}]},
1160 .wait_for_notification(
1161 comment
="wait for sIDE to process file change",
1162 method
="telemetry/event",
1165 "message": "[client-ide] Done processing file changes",
1169 comment
="Totally normal hover",
1170 method
="textDocument/hover",
1172 "textDocument": {"uri": "${php_file_uri}"},
1173 "position": {"line": 14, "character": 37},
1179 "value": "public static function staticMethod(string $z): void",
1181 'During testing, we\'ll remove the "public" tag from this '
1183 "to ensure that we can still get IDE services",
1184 "Return type: `void`",
1185 "Full name: `HoverWithErrorsClass::staticMethod`",
1188 "end": {"character": 39, "line": 14},
1189 "start": {"character": 27, "line": 14},
1192 powered_by
="serverless_ide",
1195 comment
="Remove the 'public' visibility modifier which triggers AST->AAST errors",
1196 method
="textDocument/didChange",
1198 "textDocument": {"uri": "${php_file_uri}"},
1202 "start": {"line": 10, "character": 2},
1203 "end": {"line": 10, "character": 8},
1211 comment
="Hover should still work even if visibility modifier has been removed",
1212 method
="textDocument/hover",
1214 "textDocument": {"uri": "${php_file_uri}"},
1215 "position": {"line": 14, "character": 37},
1221 "value": "public static function staticMethod(string $z): void",
1223 'During testing, we\'ll remove the "public" tag from this '
1225 "to ensure that we can still get IDE services",
1226 "Return type: `void`",
1227 "Full name: `HoverWithErrorsClass::staticMethod`",
1230 "end": {"character": 39, "line": 14},
1231 "start": {"character": 27, "line": 14},
1234 powered_by
="serverless_ide",
1236 .request(method
="shutdown", params
={}, result
=None)
1238 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1240 def test_formatting(self
) -> None:
1242 # This test will fail if hackfmt can't be found
1243 if not self
.test_driver
.run_hackfmt_check():
1244 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
1246 self
.prepare_server_environment()
1247 variables
= self
.setup_php_file("messy.php")
1248 self
.load_and_run("formatting", variables
)
1250 def test_rangeformatting(self
) -> None:
1251 # This test will fail if hackfmt can't be found
1252 if not self
.test_driver
.run_hackfmt_check():
1253 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
1255 self
.prepare_server_environment()
1256 variables
= self
.setup_php_file("messy.php")
1258 self
.initialize_spec(
1259 LspTestSpec("range_formatting"), use_serverless_ide
=False
1261 .wait_for_hh_server_ready()
1263 method
="textDocument/didOpen",
1266 "uri": "${php_file_uri}",
1267 "languageId": "hack",
1269 "text": "${php_file}",
1274 method
="textDocument/rangeFormatting",
1276 "textDocument": {"uri": "${php_file_uri}"},
1278 "start": {"line": 4, "character": 0},
1279 "end": {"line": 5, "character": 0},
1281 "options": {"tabSize": 5, "insertSpaces": True},
1286 "start": {"line": 4, "character": 0},
1287 "end": {"line": 5, "character": 0},
1289 "newText": ' $a = "this";\n',
1293 .request(method
="shutdown", params
={}, result
=None)
1295 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
1297 def test_ontypeformatting(self
) -> None:
1299 # This test will fail if hackfmt can't be found
1300 if not self
.test_driver
.run_hackfmt_check():
1301 raise unittest
.SkipTest("Hackfmt can't be found. Skipping.")
1303 self
.prepare_server_environment()
1304 variables
= self
.setup_php_file("ontypeformatting.php")
1305 self
.load_and_run("ontypeformatting", variables
)
1307 def test_did_change(self
) -> None:
1308 self
.prepare_server_environment()
1309 variables
= self
.setup_php_file("didchange.php")
1311 self
.initialize_spec(LspTestSpec("did_change"), use_serverless_ide
=False)
1312 .wait_for_hh_server_ready()
1314 method
="textDocument/didOpen",
1317 "uri": "${php_file_uri}",
1318 "languageId": "hack",
1320 "text": "${php_file}",
1325 method
="textDocument/didChange",
1327 "textDocument": {"uri": "${php_file_uri}"},
1331 "start": {"line": 7, "character": 11},
1332 "end": {"line": 7, "character": 12},
1339 .wait_for_notification(
1340 method
="textDocument/publishDiagnostics",
1342 "uri": "${php_file_uri}",
1346 "start": {"line": 7, "character": 11},
1347 "end": {"line": 7, "character": 11},
1352 "message": "A semicolon (';') is expected here.",
1353 "relatedLocations": [],
1354 "relatedInformation": [],
1359 .request(method
="shutdown", params
={}, result
=None)
1360 .wait_for_notification(
1361 comment
="Hack appears to clear out diagnostics before shutting down",
1362 method
="textDocument/publishDiagnostics",
1363 params
={"uri": "${php_file_uri}", "diagnostics": []},
1366 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
1368 def test_signature_help(self
) -> None:
1369 self
.prepare_server_environment()
1370 variables
= self
.setup_php_file("signaturehelp.php")
1372 self
.initialize_spec(
1373 LspTestSpec("test_signature_help"), use_serverless_ide
=False
1375 .wait_for_hh_server_ready()
1377 method
="textDocument/didOpen",
1380 "uri": "${php_file_uri}",
1381 "languageId": "hack",
1383 "text": "${php_file}",
1388 comment
="signature help for 0-argument constructor (left of opening paren)",
1389 method
="textDocument/signatureHelp",
1391 "textDocument": {"uri": "${php_file_uri}"},
1392 "position": {"line": 16, "character": 18},
1397 comment
="signature help for 0-argument constructor",
1398 method
="textDocument/signatureHelp",
1400 "textDocument": {"uri": "${php_file_uri}"},
1401 "position": {"line": 16, "character": 19},
1406 "label": "public function __construct(): _",
1407 "documentation": "Constructor with doc block",
1411 "activeSignature": 0,
1412 "activeParameter": 0,
1416 comment
="signature help for 0-argument constructor (right of closing paren)",
1417 method
="textDocument/signatureHelp",
1419 "textDocument": {"uri": "${php_file_uri}"},
1420 "position": {"line": 16, "character": 20},
1425 comment
="signature help for 2-argument instance method (left of opening paren)",
1426 method
="textDocument/signatureHelp",
1428 "textDocument": {"uri": "${php_file_uri}"},
1429 "position": {"line": 17, "character": 20},
1434 comment
="signature help for 2-argument instance method (right of opening paren)",
1435 method
="textDocument/signatureHelp",
1437 "textDocument": {"uri": "${php_file_uri}"},
1438 "position": {"line": 17, "character": 21},
1443 "label": "public function instanceMethod(int $x1, int $x2): void",
1444 "documentation": "Instance method with doc block",
1445 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1448 "activeSignature": 0,
1449 "activeParameter": 0,
1453 comment
="signature help for 2-argument instance method (left of first comma)",
1454 method
="textDocument/signatureHelp",
1456 "textDocument": {"uri": "${php_file_uri}"},
1457 "position": {"line": 17, "character": 22},
1462 "label": "public function instanceMethod(int $x1, int $x2): void",
1463 "documentation": "Instance method with doc block",
1464 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1467 "activeSignature": 0,
1468 "activeParameter": 1,
1472 comment
="signature help for 2-argument instance method (right of first comma)",
1473 method
="textDocument/signatureHelp",
1475 "textDocument": {"uri": "${php_file_uri}"},
1476 "position": {"line": 17, "character": 23},
1481 "label": "public function instanceMethod(int $x1, int $x2): void",
1482 "documentation": "Instance method with doc block",
1483 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1486 "activeSignature": 0,
1487 "activeParameter": 1,
1491 comment
="signature help for 2-argument instance method (left of closing paren)",
1492 method
="textDocument/signatureHelp",
1494 "textDocument": {"uri": "${php_file_uri}"},
1495 "position": {"line": 17, "character": 24},
1500 "label": "public function instanceMethod(int $x1, int $x2): void",
1501 "documentation": "Instance method with doc block",
1502 "parameters": [{"label": "$x1"}, {"label": "$x2"}],
1505 "activeSignature": 0,
1506 "activeParameter": 1,
1510 comment
="signature help for 2-argument instance method (right of closing paren)",
1511 method
="textDocument/signatureHelp",
1513 "textDocument": {"uri": "${php_file_uri}"},
1514 "position": {"line": 17, "character": 25},
1519 comment
="signature help for 1-argument static method (left of open paren)",
1520 method
="textDocument/signatureHelp",
1522 "textDocument": {"uri": "${php_file_uri}"},
1523 "position": {"line": 18, "character": 23},
1528 comment
="signature help for 1-argument static method (right of open paren)",
1529 method
="textDocument/signatureHelp",
1531 "textDocument": {"uri": "${php_file_uri}"},
1532 "position": {"line": 18, "character": 24},
1537 "label": "public static function staticMethod(string $z): void",
1538 "documentation": "Static method with doc block",
1539 "parameters": [{"label": "$z"}],
1542 "activeSignature": 0,
1543 "activeParameter": 0,
1547 comment
="signature help for 2-argument global function (left of open paren)",
1548 method
="textDocument/signatureHelp",
1550 "textDocument": {"uri": "${php_file_uri}"},
1551 "position": {"line": 19, "character": 17},
1556 comment
="signature help for 2-argument global function (right of open paren)",
1557 method
="textDocument/signatureHelp",
1559 "textDocument": {"uri": "${php_file_uri}"},
1560 "position": {"line": 19, "character": 18},
1565 "label": "function global_function(string $s, int $x): void",
1566 "documentation": "Global function with doc block",
1567 "parameters": [{"label": "$s"}, {"label": "$x"}],
1570 "activeSignature": 0,
1571 "activeParameter": 0,
1575 comment
="signature help for 1-argument namespace-aliased global function (left of open paren)",
1576 method
="textDocument/signatureHelp",
1578 "textDocument": {"uri": "${php_file_uri}"},
1579 "position": {"line": 20, "character": 26},
1584 comment
="signature help for 1-argument namespace-aliased global function (right of open paren)",
1585 method
="textDocument/signatureHelp",
1587 "textDocument": {"uri": "${php_file_uri}"},
1588 "position": {"line": 20, "character": 26},
1593 comment
="signature help for 1-argument namespace-aliased global function (right of open paren)",
1594 method
="textDocument/signatureHelp",
1596 "textDocument": {"uri": "${php_file_uri}"},
1597 "position": {"line": 20, "character": 27},
1602 "label": "function Herp\\aliased_global_func(string $s): void",
1603 "parameters": [{"label": "$s"}],
1606 "activeSignature": 0,
1607 "activeParameter": 0,
1611 comment
="signature help for 1-argument namespace-aliased global function (right of open paren)",
1612 method
="textDocument/signatureHelp",
1614 "textDocument": {"uri": "${php_file_uri}"},
1615 "position": {"line": 20, "character": 28},
1620 "label": "function Herp\\aliased_global_func(string $s): void",
1621 "parameters": [{"label": "$s"}],
1624 "activeSignature": 0,
1625 "activeParameter": 0,
1629 comment
="signature help for 2-argument function with params (right of open paren)",
1630 method
="textDocument/signatureHelp",
1632 "textDocument": {"uri": "${php_file_uri}"},
1633 "position": {"line": 21, "character": 30},
1638 "label": "function test_signature_help_params1(\n string $param1,\n string $param2\n): void",
1639 "documentation": "comment describing the method\n@param $param1 info1\n@param $param2 info2",
1641 {"label": "$param1", "documentation": "info1"},
1642 {"label": "$param2", "documentation": "info2"},
1646 "activeSignature": 0,
1647 "activeParameter": 0,
1651 comment
="signature help for 2-argument function with params (right of open paren)",
1652 method
="textDocument/signatureHelp",
1654 "textDocument": {"uri": "${php_file_uri}"},
1655 "position": {"line": 22, "character": 30},
1660 "label": "function test_signature_help_params2(\n string $param1,\n string $param2\n): void",
1661 "documentation": "comment describing the method\n@param $param1 info1",
1663 {"label": "$param1", "documentation": "info1"},
1664 {"label": "$param2"},
1668 "activeSignature": 0,
1669 "activeParameter": 0,
1673 comment
="signature help for 2-argument function with params (right of open paren)",
1674 method
="textDocument/signatureHelp",
1676 "textDocument": {"uri": "${php_file_uri}"},
1677 "position": {"line": 23, "character": 30},
1682 "label": "function test_signature_help_params3(\n string $param1,\n string $param2\n): string",
1683 "documentation": "@param $param1 info1\n for param1\n@param $param2 info2\n@return the string\n 'hack'",
1687 "documentation": "info1 for param1",
1689 {"label": "$param2", "documentation": "info2"},
1693 "activeSignature": 0,
1694 "activeParameter": 0,
1697 .request(method
="shutdown", params
={}, result
=None)
1699 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
1701 def test_rename(self
) -> None:
1702 self
.prepare_server_environment()
1703 variables
= self
.setup_php_file("rename.php")
1704 self
.load_and_run("rename", variables
)
1706 def test_references(self
) -> None:
1707 self
.prepare_server_environment()
1708 variables
= self
.setup_php_file("references.php")
1709 self
.load_and_run("references", variables
)
1711 def test_non_existing_method(self
) -> None:
1712 self
.prepare_server_environment()
1713 variables
= self
.setup_php_file("nomethod.php")
1714 self
.load_and_run("nomethod", variables
)
1716 def test_bad_call(self
) -> None:
1717 self
.prepare_server_environment()
1718 variables
= self
.setup_php_file("bad_call.php")
1719 self
.load_and_run("bad_call", variables
)
1721 def test_non_blocking(self
) -> None:
1722 self
.prepare_server_environment()
1723 variables
= self
.setup_php_file("non_blocking.php")
1724 self
.test_driver
.start_hh_loop_forever_assert_timeout()
1726 self
.initialize_spec(LspTestSpec("non_blocking"), use_serverless_ide
=False)
1727 .wait_for_hh_server_ready()
1729 method
="textDocument/definition",
1731 "textDocument": {"uri": "${php_file_uri}"},
1732 "position": {"line": 7, "character": 11},
1736 "uri": "file://${root_path}/non_blocking.php",
1738 "start": {"line": 2, "character": 9},
1739 "end": {"line": 2, "character": 32},
1741 "title": "non_blocking_definition",
1744 wait_id
="definition request",
1747 comment
="remove hh_loop_forever() invocation to break the infinite loop",
1748 method
="textDocument/didOpen",
1751 "uri": "${root_path}/__hh_loop_forever_foo.php",
1752 "languageId": "hack",
1757 function __hh_loop_forever_foo(): int {
1764 .wait_for_response(wait_id
="definition request")
1765 .request(method
="shutdown", params
={}, result
=None)
1767 self
.run_spec(spec
, variables
, wait_for_server
=True, use_serverless_ide
=False)
1769 def test_serverless_ide_hierarchy_file_change_on_disk(self
) -> None:
1770 variables
= dict(self
.prepare_serverless_ide_environment())
1771 variables
.update(self
.setup_php_file("incremental_derived.php"))
1772 changed_php_file_uri
= self
.repo_file("incremental_base.php")
1773 variables
.update({"changed_php_file_uri": changed_php_file_uri
})
1774 self
.test_driver
.stop_hh_server()
1777 self
.initialize_spec(
1778 LspTestSpec("serverless_ide_hierarchy_file_change_on_disk"),
1779 use_serverless_ide
=True,
1782 method
="textDocument/didOpen",
1785 "uri": "${php_file_uri}",
1786 "languageId": "hack",
1788 "text": "${php_file}",
1793 comment
="hover before change to class hierarchy should be `int`",
1794 method
="textDocument/hover",
1796 "textDocument": {"uri": "${php_file_uri}"},
1797 "position": {"line": 7, "character": 14},
1801 {"language": "hack", "value": "public function foo(): int"},
1802 "Return type: `int`",
1803 "Full name: `BaseClassIncremental::foo`",
1806 "start": {"line": 7, "character": 12},
1807 "end": {"line": 7, "character": 15},
1810 powered_by
="serverless_ide",
1813 uri
=changed_php_file_uri
,
1816 class BaseClassIncremental {
1817 public function foo(): string { return ''; }
1822 .wait_for_notification(
1823 comment
="wait for sIDE to process file change",
1824 method
="telemetry/event",
1827 "message": "[client-ide] Done processing file changes",
1831 comment
="hover after change to class hierarchy should be `string`",
1832 method
="textDocument/hover",
1834 "textDocument": {"uri": "${php_file_uri}"},
1835 "position": {"line": 7, "character": 14},
1839 {"language": "hack", "value": "public function foo(): string"},
1840 "Return type: `string`",
1841 "Full name: `BaseClassIncremental::foo`",
1844 "start": {"line": 7, "character": 12},
1845 "end": {"line": 7, "character": 15},
1848 powered_by
="serverless_ide",
1850 .request(method
="shutdown", params
={}, result
=None)
1853 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1855 def test_serverless_ide_decl_in_unsaved_buffer_changed(self
) -> None:
1856 variables
= dict(self
.prepare_serverless_ide_environment())
1857 variables
.update(self
.setup_php_file("hover.php"))
1858 self
.test_driver
.stop_hh_server()
1861 self
.initialize_spec(
1862 LspTestSpec("serverless_ide_decl_in_unsaved_buffer_changed"),
1863 use_serverless_ide
=True,
1866 method
="textDocument/didOpen",
1869 "uri": "${php_file_uri}",
1870 "languageId": "hack",
1872 "text": "${php_file}",
1877 comment
="hover over function invocation",
1878 method
="textDocument/hover",
1880 "textDocument": {"uri": "${php_file_uri}"},
1881 "position": {"line": 3, "character": 16},
1885 {"language": "hack", "value": "int"},
1886 "A comment describing b_hover.",
1889 "start": {"line": 3, "character": 9},
1890 "end": {"line": 3, "character": 16},
1893 powered_by
="serverless_ide",
1896 comment
="make local, unsaved change to the file",
1897 method
="textDocument/didChange",
1899 "textDocument": {"uri": "${php_file_uri}", "version": 2},
1905 function a_hover(): int {
1908 # A comment describing b_hover.
1909 function b_hover(): string {
1918 comment
="another hover over function invocation, should be string now",
1919 method
="textDocument/hover",
1921 "textDocument": {"uri": "${php_file_uri}"},
1922 "position": {"line": 3, "character": 16},
1926 {"language": "hack", "value": "string"},
1927 "A comment describing b_hover.",
1930 "start": {"line": 3, "character": 9},
1931 "end": {"line": 3, "character": 16},
1934 powered_by
="serverless_ide",
1936 .request(method
="shutdown", params
={}, result
=None)
1939 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
1941 def _sanitize_gutter_line_numbers(self
, s
: str) -> str:
1942 gutter_line_number_re
= re
.compile(r
"^[ ]*[0-9]+ \|", re
.MULTILINE
)
1943 return re
.sub(gutter_line_number_re
, " XXXX |", s
)
1945 def test_lsptestspec_incorrect_request_result(self
) -> None:
1946 variables
= dict(self
.prepare_serverless_ide_environment())
1947 variables
.update(self
.setup_php_file("hover.php"))
1948 self
.test_driver
.stop_hh_server()
1951 self
.initialize_spec(LspTestSpec("bad_hover"), use_serverless_ide
=True)
1953 method
="textDocument/didOpen",
1956 "uri": "${php_file_uri}",
1957 "languageId": "hack",
1959 "text": "${php_file}",
1964 comment
="hover over function invocation",
1965 method
="textDocument/hover",
1967 "textDocument": {"uri": "${php_file_uri}"},
1968 "position": {"line": 3, "character": 16},
1972 {"language": "hack", "value": "int"},
1973 "INCORRECT COMMENT HERE",
1976 "start": {"line": 3, "character": 9},
1977 "end": {"line": 3, "character": 16},
1980 powered_by
="serverless_ide",
1982 .request(method
="shutdown", params
={}, result
=None)
1987 variables
=variables
,
1988 wait_for_server
=False,
1989 use_serverless_ide
=True,
1991 assert False, "No assertion failure raised"
1992 except AssertionError as e
:
1994 self
._sanitize
_gutter
_line
_numbers
(str(e
)),
1996 Test case bad_hover failed with 1 errors:
1999 Description: Request with ID 4 (comment: 'hover over function invocation') \
2000 got an incorrect result:
2002 (+ is expected lines, - is actual lines)
2003 - {'contents': [{'language': 'hack', 'value': 'int'},
2004 + {'contents': [{'language': 'hack', 'value': 'int'}, 'INCORRECT COMMENT HERE'],
2005 ? +++++++++++++++++++++++++++
2007 - 'A comment describing b_hover.'],
2008 'range': {'end': {'character': 16, 'line': 3},
2009 'start': {'character': 9, 'line': 3}}}
2012 This was the associated request:
2014 hphp/hack/test/integration/test_lsp.py
2016 XXXX | comment="hover over function invocation",
2017 XXXX | method="textDocument/hover",
2019 XXXX | "textDocument": {"uri": "${php_file_uri}"},
2020 XXXX | "position": {"line": 3, "character": 16},
2023 XXXX | "contents": [
2024 XXXX | {"language": "hack", "value": "int"},
2025 XXXX | "INCORRECT COMMENT HERE",
2028 XXXX | "start": {"line": 3, "character": 9},
2029 XXXX | "end": {"line": 3, "character": 16},
2032 XXXX | powered_by="serverless_ide",
2036 1) If this was unexpected, then the language server is buggy and should be
2039 2) If this was expected, you can update your request with the following code to
2043 comment='hover over function invocation',
2044 method='textDocument/hover',
2045 params={'textDocument': {'uri': '${php_file_uri}'}, \
2046 'position': {'line': 3, 'character': 16}},
2047 result={'contents': [{'language': 'hack', 'value': 'int'}, \
2048 'A comment describing b_hover.'], \
2049 'range': {'start': {'line': 3, 'character': 9}, \
2050 'end': {'line': 3, 'character': 16}}},
2051 powered_by='serverless_ide',
2054 If you want to examine the raw LSP logs, you can check the `.sent.log` and
2055 `.received.log` files that were generated in the template repo for this test.\
2059 def test_lsptestspec_unexpected_notification(self
) -> None:
2060 self
.prepare_server_environment()
2061 variables
= self
.setup_php_file("didchange.php")
2063 self
.initialize_spec(LspTestSpec("did_change"), use_serverless_ide
=False)
2064 .wait_for_hh_server_ready()
2066 method
="textDocument/didOpen",
2069 "uri": "${php_file_uri}",
2070 "languageId": "hack",
2072 "text": "${php_file}",
2077 method
="textDocument/didChange",
2079 "textDocument": {"uri": "${php_file_uri}"},
2083 "start": {"line": 7, "character": 11},
2084 "end": {"line": 7, "character": 12},
2091 .wait_for_notification(
2092 method
="textDocument/publishDiagnostics",
2094 "uri": "${php_file_uri}",
2098 "start": {"line": 7, "character": 11},
2099 "end": {"line": 7, "character": 11},
2104 "message": "A semicolon (';') is expected here.",
2105 "relatedLocations": [],
2106 "relatedInformation": [],
2111 .request(method
="shutdown", params
={}, result
=None)
2115 spec
, variables
, wait_for_server
=True, use_serverless_ide
=False
2117 assert False, "No assertion failure raised"
2118 except AssertionError as e
:
2120 self
._sanitize
_gutter
_line
_numbers
(str(e
)),
2122 Test case did_change failed with 1 errors:
2125 Description: An unexpected notification of type \
2126 'textDocument/publishDiagnostics' was sent by the language server.
2127 Here is the notification payload:
2130 'method': 'textDocument/publishDiagnostics',
2131 'params': {'diagnostics': [],
2132 'uri': '__PHP_FILE_URI__'}}
2135 This was the most recent request issued from the language client before it
2136 received the notification:
2138 hphp/hack/test/integration/test_lsp.py
2139 XXXX | .request(method="shutdown", params={}, result=None)
2142 1) If this was unexpected, then the language server is buggy and should be
2145 2) If all notifications of type 'textDocument/publishDiagnostics' should be \
2146 ignored, add this directive
2147 anywhere in your test:
2149 .ignore_notifications(method='textDocument/publishDiagnostics')
2151 3) If this single instance of the notification was expected, add this directive
2152 to your test to wait for it before proceeding:
2154 .wait_for_notification(
2155 method='textDocument/publishDiagnostics',
2156 params={'uri': '${php_file_uri}', 'diagnostics': []},
2159 If you want to examine the raw LSP logs, you can check the `.sent.log` and
2160 `.received.log` files that were generated in the template repo for this test.\
2162 # There's an instance of a literal `${php_file_uri}` in there
2163 # which we don't want to change, so use a different name than
2165 .replace("__PHP_FILE_URI__", variables
["php_file_uri"]),
2168 def test_serverless_ide_highlight(self
) -> None:
2169 variables
= dict(self
.prepare_serverless_ide_environment())
2170 variables
.update(self
.setup_php_file("highlight.php"))
2171 self
.test_driver
.stop_hh_server()
2174 self
.initialize_spec(
2175 LspTestSpec("serverless_ide_highlight"), use_serverless_ide
=True
2178 method
="textDocument/didOpen",
2181 "uri": "${php_file_uri}",
2182 "languageId": "hack",
2184 "text": "${php_file}",
2189 comment
="document highlight, id 2",
2190 method
="textDocument/documentHighlight",
2192 "textDocument": {"uri": "${php_file_uri}"},
2193 "position": {"line": 3, "character": 10},
2198 "start": {"line": 3, "character": 9},
2199 "end": {"line": 3, "character": 20},
2205 comment
="shutdown, id 3", method
="shutdown", params
={}, result
=None
2208 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)
2210 def test_serverless_ide_coverage(self
) -> None:
2211 variables
= dict(self
.prepare_serverless_ide_environment())
2212 variables
.update(self
.setup_php_file("coverage.php"))
2213 self
.test_driver
.stop_hh_server()
2216 self
.initialize_spec(
2217 LspTestSpec("serverless_ide_coverage"), use_serverless_ide
=True
2220 method
="textDocument/didOpen",
2223 "uri": "${php_file_uri}",
2224 "languageId": "hack",
2226 "text": "${php_file}",
2231 comment
="Check type coverage",
2232 method
="textDocument/typeCoverage",
2233 params
={"textDocument": {"uri": "${php_file_uri}"}},
2235 "coveredPercent": 100,
2236 "uncoveredRanges": [],
2237 "defaultMessage": "Un-type checked code. Consider adding "
2238 "type annotations.",
2241 .request(comment
="Shutdown", method
="shutdown", params
={}, result
=None)
2243 self
.run_spec(spec
, variables
, wait_for_server
=False, use_serverless_ide
=True)