3 from __future__
import absolute_import
, division
, print_function
, unicode_literals
14 from typing
import ClassVar
, List
, Mapping
, Optional
, Tuple
16 from hh_paths
import hackfmt
, hh_client
, hh_merge_deps
, hh_server
17 from test_case
import TestCase
, TestDriver
18 from utils
import Json
, JsonObject
21 class DebugSubscription(object):
23 Wraps `hh_client debug`.
26 # pyre-fixme[24]: Generic type `subprocess.Popen` expects 1 type parameter.
27 def __init__(self
, proc
: subprocess
.Popen
) -> None:
29 hello
= self
.read_msg()
30 assert hello
["data"] == "hello"
32 # pyre-fixme[11]: Annotation `Json` is not defined as a type.
33 def read_msg(self
) -> Json
:
34 # pyre-fixme[16]: `Optional` has no attribute `readline`.
35 line
= self
.proc
.stdout
.readline()
36 return json
.loads(line
)
38 # pyre-fixme[11]: Annotation `JsonObject` is not defined as a type.
39 def get_incremental_logs(self
) -> JsonObject
:
43 if msg
["type"] == "info" and msg
["data"] == "incremental_done":
45 msgs
[msg
["name"]] = msg
49 class CommonTestDriver(TestDriver
):
51 # This needs to be overridden in child classes. The files in this
52 # directory will be used to set up the initial environment for each
54 template_repo
: ClassVar
[str]
55 repo_dir
: ClassVar
[str]
56 test_env
: ClassVar
[Mapping
[str, str]]
57 base_tmp_dir
: ClassVar
[str]
58 hh_tmp_dir
: ClassVar
[str]
59 bin_dir
: ClassVar
[str]
62 def setUpClass(cls
, template_repo
: str) -> None:
63 cls
.template_repo
= template_repo
65 cls
.base_tmp_dir
= tempfile
.mkdtemp()
66 # we don't create repo_dir using mkdtemp() because we want to create
67 # it with copytree(). copytree() will fail if the directory already
69 cls
.repo_dir
= os
.path
.join(cls
.base_tmp_dir
, "repo")
70 # Where the hhi files, socket, etc get extracted
71 cls
.hh_tmp_dir
= tempfile
.mkdtemp()
72 cls
.bin_dir
= tempfile
.mkdtemp()
73 hh_server_dir
= os
.path
.dirname(hh_server
)
75 hh_merge_deps_dir
= os
.path
.dirname(hh_merge_deps
)
76 print("hh_server_dir " + hh_server_dir
)
77 print("hh_merge_deps_dir " + hh_merge_deps_dir
)
83 "HH_TMPDIR": cls
.hh_tmp_dir
,
85 "%s:%s:%s:/bin:/usr/bin:/usr/local/bin"
86 % (hh_server_dir
, hh_merge_deps_dir
, cls
.bin_dir
)
88 "HH_HOME": os
.path
.dirname(hh_client
),
90 "HH_LOCALCONF_PATH": cls
.repo_dir
,
95 def tearDownClass(cls
) -> None:
96 shutil
.rmtree(cls
.base_tmp_dir
)
97 shutil
.rmtree(cls
.bin_dir
)
98 shutil
.rmtree(cls
.hh_tmp_dir
)
100 def write_load_config(self
, use_saved_state
: bool = False) -> None:
102 Writes out a script that will print the list of changed files,
103 and adds the path to that script to .hhconfig
105 raise NotImplementedError()
107 def wait_until_server_ready(self
) -> None:
109 We don't want to accidentally connect to an old hh_server, so we wait 2
110 seconds for the monitor to start up the new server first.
117 changed_files
: Optional
[List
[str]] = None,
118 saved_state_path
: Optional
[str] = None,
119 args
: Optional
[List
[str]] = None,
121 """ Start an hh_server. changed_files is ignored here (as it
122 has no meaning) and is only exposed in this API for the derived
125 if changed_files
is None:
129 cmd
= [hh_server
, "--daemon", "--max-procs", "2", self
.repo_dir
] + args
131 self
.wait_until_server_ready()
133 def stop_hh_server(self
, retries
: int = 3) -> None:
134 (_
, _
, exit_code
) = self
.proc_call([hh_client
, "stop", self
.repo_dir
])
137 elif retries
> 0 and exit_code
!= 0:
138 self
.stop_hh_server(retries
=retries
- 1)
140 self
.assertEqual(exit_code
, 0, msg
="Stopping hh_server failed")
142 def get_server_logs(self
) -> str:
143 time
.sleep(2) # wait for logs to be written
144 log_file
= self
.proc_call([hh_client
, "--logname", self
.repo_dir
])[0].strip()
145 with
open(log_file
) as f
:
148 def get_monitor_logs(self
) -> str:
149 time
.sleep(2) # wait for logs to be written
150 log_file
= self
.proc_call([hh_client
, "--monitor-logname", self
.repo_dir
])[
153 with
open(log_file
) as f
:
156 def setUp(self
) -> None:
157 shutil
.copytree(self
.template_repo
, self
.repo_dir
)
159 def tearDownWithRetries(self
, retries
: int = 3) -> None:
160 self
.stop_hh_server(retries
=retries
)
161 shutil
.rmtree(self
.repo_dir
)
163 def tearDown(self
) -> None:
164 self
.tearDownWithRetries()
167 # pyre-fixme[24]: Generic type `subprocess.Popen` expects 1 type parameter.
168 def proc_create(cls
, args
: List
[str], env
: Mapping
[str, str]) -> subprocess
.Popen
:
169 return subprocess
.Popen(
171 stdin
=subprocess
.PIPE
,
172 stdout
=subprocess
.PIPE
,
173 stderr
=subprocess
.PIPE
,
174 env
=dict(cls
.test_env
, **env
),
175 universal_newlines
=True,
182 env
: Optional
[Mapping
[str, str]] = None,
183 stdin
: Optional
[str] = None,
184 ) -> Tuple
[str, str, int]:
186 Invoke a subprocess, return stdout, send stderr to our stderr (for
189 env
= {} if env
is None else env
190 print(" ".join(args
), file=sys
.stderr
)
191 proc
= cls
.proc_create(args
, env
)
192 (stdout_data
, stderr_data
) = proc
.communicate(stdin
)
193 sys
.stderr
.write(stderr_data
)
195 retcode
= proc
.wait()
196 return (stdout_data
, stderr_data
, retcode
)
199 def wait_pid_with_timeout(cls
, pid
: int, timeout
: int) -> None:
201 Like os.waitpid but with a timeout in seconds.
205 pid_expected
, _
= os
.waitpid(pid
, os
.WNOHANG
)
206 if pid_expected
== pid
:
208 elif waited_time
>= timeout
:
209 raise subprocess
.TimeoutExpired
215 self
, stdin
: Optional
[str] = None, options
: Optional
[List
[str]] = None
216 ) -> Tuple
[str, str, int]:
217 options
= [] if options
is None else options
218 root
= self
.repo_dir
+ os
.path
.sep
219 return self
.proc_call(
229 + list(map(lambda x
: x
.format(root
=root
), options
)),
233 # Check to see if you can run hackfmt
234 def run_hackfmt_check(self
) -> bool:
237 (stdout_data
, stderr_data
, retcode
) = self
.proc_call(["hackfmt", "-help"])
239 # If the file isn't found you will get this
240 except FileNotFoundError
:
245 stdin
: Optional
[str] = None,
246 options
: Optional
[List
[str]] = None,
247 expected_output
: Optional
[str] = None,
249 options
= [] if options
is None else options
250 (output
, err
, retcode
) = self
.proc_call([hackfmt
] + options
, stdin
=stdin
)
252 print("check returned non-zero code: " + str(retcode
), file=sys
.stderr
)
253 if expected_output
is not None:
254 self
.assertEqual(expected_output
, output
)
257 # Runs `hh_client check` asserting the stdout is equal the expected.
259 # Note: assert_laoded_mini_state is ignored here and only used
260 # in some derived classes.
263 expected_output
: Optional
[List
[str]],
264 stdin
: Optional
[str] = None,
265 options
: Optional
[List
[str]] = None,
266 assert_loaded_saved_state
: bool = False,
268 (output
, err
, retcode
) = self
.run_check(stdin
, options
)
269 root
= self
.repo_dir
+ os
.path
.sep
271 print("check returned non-zero code: " + str(retcode
), file=sys
.stderr
)
272 if expected_output
is not None:
273 # pyre-fixme[8]: Attribute has type `int`; used as `None`.
275 self
.assertCountEqual(
276 map(lambda x
: x
.format(root
=root
), expected_output
), output
.splitlines()
280 def check_cmd_and_json_cmd(
282 expected_output
: List
[str],
283 expected_json
: List
[str],
284 stdin
: Optional
[str] = None,
285 options
: Optional
[List
[str]] = None,
287 # we run the --json version first because --json --refactor doesn't
288 # change any files, but plain --refactor does (i.e. the latter isn't
292 self
.check_cmd(expected_json
, stdin
, options
+ ["--json"])
293 self
.check_cmd(expected_output
, stdin
, options
)
295 def subscribe_debug(self
) -> DebugSubscription
:
296 proc
= self
.proc_create([hh_client
, "debug", self
.repo_dir
], env
={})
297 return DebugSubscription(proc
)
299 def start_hh_loop_forever_assert_timeout(self
) -> None:
300 # create a file with 10 dependencies. Only "big" jobs, that use
301 # workers can be interrupted at the moment.
302 with
open(os
.path
.join(self
.repo_dir
, "__hh_loop_forever_foo.php"), "w") as f
:
305 function __hh_loop_forever_foo(): int {
310 for i
in range(1, 10):
312 os
.path
.join(self
.repo_dir
, "__hh_loop_forever_bar%d.php" % i
), "w"
316 function __hh_loop_forever_bar%d(): int {
317 return __hh_loop_forever_foo();
322 self
.check_cmd(["No errors!"])
324 # trigger rechecking of all 11 files, and make one of them loop
326 with
open(os
.path
.join(self
.repo_dir
, "__hh_loop_forever_foo.php"), "w") as f
:
329 function __hh_loop_forever_foo(): string {
334 # this should timeout due to infinite loop
336 # empty output means no results due to timeout
337 self
.check_cmd([], options
=["--retries", "1"])
338 except AssertionError:
339 # one of the test drivers doesn't like timeouts
342 def stop_hh_loop_forever(self
) -> None:
343 # subsequent change should interrupt the "loop forever" part
344 with
open(os
.path
.join(self
.repo_dir
, "__hh_loop_forever_foo.php"), "w") as f
:
347 function __hh_loop_forever_foo(): int {
352 self
.check_cmd(["No errors!"])
355 # The most basic of tests.
356 # Exercises server responsiveness, and updating errors after changing files
357 class BarebonesTests(TestCase
[CommonTestDriver
]):
359 # hh should should work with 0 retries.
360 def test_responsiveness(self
) -> None:
361 self
.test_driver
.start_hh_server()
362 self
.test_driver
.check_cmd(["No errors!"])
363 self
.test_driver
.check_cmd(["No errors!"], options
=["--retries", "0"])
365 def test_new_file(self
) -> None:
367 Add a new file that contains an error.
369 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
379 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php"])
381 self
.test_driver
.check_cmd(
383 "{root}foo_4.php:4:24,26: Invalid return type (Typing[4110])",
384 " {root}foo_4.php:3:27,29: Expected `int`",
385 " {root}foo_4.php:4:24,26: But got `string`",
389 def test_new_naming_error(self
) -> None:
391 Add a new file which contains a naming collisions with an old file
393 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
402 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php"])
404 self
.test_driver
.check_cmd(
406 "{root}foo_4.php:3:19,21: Name already bound: `FOO` (Naming[2012])",
407 " {root}foo_3.php:7:15,17: Previous definition `F~~oo~~` differs only by case ",
408 "{root}foo_4.php:4:22,22: Name already bound: `H` (Naming[2012])",
409 " {root}foo_3.php:3:18,18: Previous definition `~~h~~` differs only by case ",
413 # We put this test in Barebones tests so that dependencies on class B
414 # show an error (i.e. class_3.php) with both the save state driver
415 # and the classic save state driver
416 def test_modify_extends_deps(self
) -> None:
418 Introduce a change to a base class that causes an error
419 in a use case on one of its subclasses.
421 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "class_1.php"), "w") as f
:
426 public static function foo () : bool {
432 self
.test_driver
.start_hh_server(changed_files
=["class_1.php"])
433 self
.test_driver
.check_cmd(
435 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
436 " {root}class_3.php:4:28,30: Expected `int`",
437 " {root}class_1.php:4:51,54: But got `bool`",
442 # Common tests, includes the Barebones Tests above
443 class CommonTests(BarebonesTests
):
444 def test_json_errors(self
) -> None:
446 If you ask for errors in JSON format, you will get them on standard
447 output. Changing this will break the tools that depend on it (like
448 editor plugins), and this test is here to remind you about it.
450 self
.test_driver
.start_hh_server()
452 stderr
= self
.test_driver
.check_cmd([], options
=["--json"])
453 last_line
= stderr
.splitlines()[-1]
454 output
= json
.loads(last_line
)
456 self
.assertEqual(output
["errors"], [])
457 self
.assertEqual(output
["passed"], True)
458 self
.assertIn("version", output
)
460 def test_modify_file(self
) -> None:
462 Add an error to a file that previously had none.
464 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"), "w") as f
:
474 self
.test_driver
.start_hh_server(changed_files
=["foo_2.php"])
476 self
.test_driver
.check_cmd(
478 "{root}foo_2.php:4:24,26: Invalid return type (Typing[4110])",
479 " {root}foo_2.php:3:27,29: Expected `int`",
480 " {root}foo_2.php:4:24,26: But got `string`",
484 def test_deleted_file(self
) -> None:
486 Delete a file that still has dangling references before restoring from
489 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"))
491 self
.test_driver
.start_hh_server(changed_files
=["foo_2.php"])
493 self
.test_driver
.check_cmd(
495 "{root}foo_1.php:4:20,20: Unbound name: `g` (a global function) (Naming[2049])"
499 def test_file_delete_after_load(self
) -> None:
501 Delete a file that still has dangling references after restoring from
504 self
.test_driver
.start_hh_server()
505 self
.test_driver
.check_cmd(["No errors!"])
506 debug_sub
= self
.test_driver
.subscribe_debug()
508 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"))
509 msgs
= debug_sub
.get_incremental_logs()
510 self
.assertEqual(msgs
["to_redecl_phase1"]["files"], ["foo_2.php"])
511 self
.assertEqual(msgs
["to_redecl_phase2"]["files"], ["foo_2.php"])
513 set(msgs
["to_recheck"]["files"]), set(["foo_1.php", "foo_2.php"])
515 self
.test_driver
.check_cmd(
517 "{root}foo_1.php:4:20,20: Unbound name: `g` (a global function) (Naming[2049])"
521 def test_duplicated_file(self
) -> None:
522 self
.test_driver
.start_hh_server(changed_files
=["foo_2.php"])
523 self
.test_driver
.check_cmd(["No errors!"])
526 os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"),
527 os
.path
.join(self
.test_driver
.repo_dir
, "foo_2_dup.php"),
530 self
.test_driver
.check_cmd(
532 "{root}foo_2_dup.php:3:18,18: Name already bound: `g` (Naming[2012])",
533 " {root}foo_2.php:3:18,18: Previous definition is here",
537 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"))
538 self
.test_driver
.check_cmd(["No errors!"])
540 def test_moved_file(self
) -> None:
542 Move a file, then create an error that references a definition in it.
543 Check that the new file name is displayed in the error.
546 self
.test_driver
.start_hh_server(
547 changed_files
=["foo_1.php", "foo_2.php", "bar_2.php"]
551 os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"),
552 os
.path
.join(self
.test_driver
.repo_dir
, "bar_2.php"),
555 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_1.php"), "w") as f
:
559 function f(): string {
565 self
.test_driver
.check_cmd(
567 "{root}foo_1.php:4:24,26: Invalid return type (Typing[4110])",
568 " {root}foo_1.php:3:27,32: Expected `string`",
569 " {root}bar_2.php:3:23,25: But got `int`",
573 def test_find_refs(self
) -> None:
575 Test hh_client --find-refs, --find-class-refs
577 self
.test_driver
.start_hh_server()
579 self
.test_driver
.check_cmd_and_json_cmd(
580 ['File "{root}foo_3.php", line 11, characters 13-13: h', "1 total results"],
582 '[{{"name":"h","filename":"{root}foo_3.php","line":11,"char_start":13,"char_end":13}}]'
584 options
=["--find-refs", "h"],
587 self
.test_driver
.check_cmd_and_json_cmd(
589 'File "{root}foo_3.php", line 10, characters 17-19: Foo::__construct',
593 '[{{"name":"Foo::__construct","filename":"{root}foo_3.php","line":10,"char_start":17,"char_end":19}}]'
595 options
=["--find-refs", "Foo::__construct"],
598 self
.test_driver
.check_cmd_and_json_cmd(
600 'File "{root}foo_3.php", line 10, characters 17-19: Foo',
604 '[{{"name":"Foo","filename":"{root}foo_3.php","line":10,'
605 '"char_start":17,"char_end":19}}]'
607 options
=["--find-class-refs", "Foo"],
610 def test_identify_symbol(self
) -> None:
612 Test hh_client --identify
614 self
.test_driver
.start_hh_server()
616 self
.test_driver
.check_cmd_and_json_cmd(
619 '[{{"full_name":"B","pos":{{"filename":"{root}class_1.php","line":3,"char_start":7,"char_end":7}},"kind":"class"}}]'
621 options
=["--identify", "B"],
624 self
.test_driver
.check_cmd_and_json_cmd(
627 '[{{"full_name":"B::foo","pos":{{"filename":"{root}class_1.php","line":5,"char_start":26,"char_end":28}},"kind":"method"}}]'
629 options
=["--identify", "B::foo"],
632 self
.test_driver
.check_cmd_and_json_cmd(
635 '[{{"full_name":"CONST_SOME_COOL_VALUE","pos":{{"filename":"{root}const_1.php","line":3,"char_start":11,"char_end":31}},"kind":"const"}}]'
637 options
=["--identify", "CONST_SOME_COOL_VALUE"],
640 self
.test_driver
.check_cmd_and_json_cmd(
643 '[{{"full_name":"FbidMapField","pos":{{"filename":"{root}enum_1.php","line":3,"char_start":6,"char_end":17}},"kind":"enum"}}]'
645 options
=["--identify", "FbidMapField"],
648 self
.test_driver
.check_cmd_and_json_cmd(
651 '[{{"full_name":"FbidMapField::FBID","pos":{{"filename":"{root}enum_1.php","line":4,"char_start":3,"char_end":6}},"kind":"const"}}]'
653 options
=["--identify", "FbidMapField::FBID"],
656 self
.test_driver
.check_cmd_and_json_cmd(
659 '[{{"full_name":"f","pos":{{"filename":"{root}foo_1.php","line":3,"char_start":18,"char_end":18}},"kind":"function"}}]'
661 options
=["--identify", "f"],
664 def test_ide_find_refs(self
) -> None:
665 self
.test_driver
.start_hh_server()
667 self
.test_driver
.check_cmd_and_json_cmd(
670 'File "{root}foo_3.php", line 10, characters 17-19:',
674 '[{{"name":"Foo","filename":"{root}foo_3.php",'
675 '"line":10,"char_start":17,"char_end":19}}]'
677 options
=["--ide-find-refs", "1:20"],
678 stdin
="<?hh function test(Foo $foo) { new Foo(); }",
681 def test_ide_highlight_refs(self
) -> None:
682 self
.test_driver
.start_hh_server()
684 self
.test_driver
.check_cmd_and_json_cmd(
685 ["line 1, characters 20-22", "line 1, characters 36-38", "2 total results"],
687 '[{{"line":1,"char_start":20,"char_end":22}},'
688 '{{"line":1,"char_start":36,"char_end":38}}]'
690 options
=["--ide-highlight-refs", "1:20"],
691 stdin
="<?hh function test(Foo $foo) { new Foo(); }",
694 def test_search(self
) -> None:
696 Test hh_client --search
699 self
.test_driver
.start_hh_server()
701 self
.test_driver
.check_cmd_and_json_cmd(
703 'File "{root}foo_3.php", line 9, characters 18-40: some_long_function_name, function'
706 '[{{"name":"some_long_function_name","filename":"{root}foo_3.php","desc":"function","line":9,"char_start":18,"char_end":40,"scope":""}}]'
708 options
=["--search", "some_lo"],
711 def test_search_case_insensitive1(self
) -> None:
713 Test that global search is not case sensitive
716 self
.test_driver
.start_hh_server()
718 self
.test_driver
.check_cmd(
720 'File "{root}foo_4.php", line 4, characters 10-24: '
721 "aaaaaaaaaaa_fun, function",
722 'File "{root}foo_4.php", line 3, characters 7-23: '
723 "Aaaaaaaaaaa_class, class",
725 options
=["--search", "Aaaaaaaaaaa"],
728 def test_search_case_insensitive2(self
) -> None:
730 Test that global search is not case sensitive
732 self
.test_driver
.start_hh_server()
734 self
.test_driver
.check_cmd(
736 'File "{root}foo_4.php", line 4, characters 10-24: '
737 "aaaaaaaaaaa_fun, function",
738 'File "{root}foo_4.php", line 3, characters 7-23: '
739 "Aaaaaaaaaaa_class, class",
741 options
=["--search", "aaaaaaaaaaa"],
744 def test_auto_complete(self
) -> None:
746 Test hh_client --auto-complete
749 self
.test_driver
.start_hh_server()
751 self
.test_driver
.check_cmd_and_json_cmd(
752 ["some_long_function_name (function(): _)"],
754 # test the --json output because the non-json one doesn't contain
755 # the filename, and we are especially interested in testing file
757 # the doubled curly braces are because this string gets passed
759 '[{{"name":"some_long_function_name",'
760 '"type":"(function(): _)",'
761 '"pos":{{"filename":"{root}foo_3.php",'
762 '"line":9,"char_start":18,"char_end":40}},'
763 '"func_details":{{"min_arity":0,"return_type":"_","params":[]}},'
764 '"expected_ty":false}}]'
766 options
=["--auto-complete"],
767 stdin
="<?hh function f() { some_AUTO332\n",
770 def test_list_files(self
) -> None:
772 Test hh_client --list-files
774 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_2.php"))
775 self
.test_driver
.start_hh_server(changed_files
=["foo_2.php"])
776 self
.test_driver
.check_cmd_and_json_cmd(
778 ["{root}foo_1.php"], # see comment for identify-function
779 options
=["--list-files"],
782 def test_type_at_pos(self
) -> None:
784 Test hh_client --type-at-pos
786 self
.test_driver
.start_hh_server()
788 self
.test_driver
.check_cmd_and_json_cmd(
792 + '"pos":{{"filename":"","line":0,"char_start":0,"char_end":0}},'
793 + '"full_type":{{"src_pos":{{"filename":"{root}foo_3.php","line":3,"char_start":23,"char_end":28}},"kind":"primitive","name":"string"}}}}'
795 options
=["--type-at-pos", "{root}foo_3.php:11:14"],
798 def test_type_at_pos_batch(self
) -> None:
800 Test hh_client --type-at-pos-batch
802 self
.test_driver
.start_hh_server()
804 self
.test_driver
.check_cmd(
807 + '{{"file":"{root}foo_3.php",'
811 + '"src_pos":{{"filename":"{root}foo_3.php","line":3,"char_start":23,"char_end":28}},'
812 + '"kind":"primitive",'
813 + '"name":"string"}}}}'
815 options
=["--type-at-pos-batch", "{root}foo_3.php:11:14"],
818 def test_ide_get_definition(self
) -> None:
820 Test hh_client --ide-get-definition
822 self
.test_driver
.start_hh_server()
824 self
.test_driver
.check_cmd_and_json_cmd(
826 "name: \\bar, kind: function, span: line 1, characters 42-44,"
827 " is_declaration: false",
831 " id: function::bar",
832 ' position: File "", line 1, characters 15-17:',
833 ' span: File "", line 1, character 6 - line 1, character 22:',
840 '[{{"name":"\\\\bar","result_type":"function",'
841 '"pos":{{"filename":"","line":1,"char_start":42,"char_end":44}},'
842 '"definition_pos":{{"filename":"","line":1,"char_start":15,'
843 '"char_end":17}},"definition_span":{{"filename":"","line_start":1,'
844 '"char_start":6,"line_end":1,"char_end":22}},'
845 '"definition_id":"function::bar"}}]'
847 options
=["--ide-get-definition", "1:43"],
848 stdin
="<?hh function bar() {} function test() { bar() }",
851 def test_ide_outline(self
) -> None:
853 Test hh_client --ide-outline
855 self
.test_driver
.start_hh_server()
858 This call is here to ensure that server is running. Outline command
859 doesn't require it to be, but integration test suite assumes it and
860 checks it's state after each test.
862 self
.test_driver
.check_cmd(["No errors!"])
864 self
.test_driver
.check_cmd_and_json_cmd(
868 " id: function::bar",
869 ' position: File "", line 1, characters 15-17:',
870 ' span: File "", line 1, character 6 - line 1, character 22:',
876 '[{{"kind":"function","name":"bar","id":"function::bar",'
877 '"position":{{"filename":"",'
878 '"line":1,"char_start":15,"char_end":17}},"span":'
879 '{{"filename":"","line_start":1,"char_start":6,"line_end":1,'
880 '"char_end":22}},"modifiers":[],"params":[]}}]'
882 options
=["--ide-outline"],
883 stdin
="<?hh function bar() {}",
886 def test_ide_get_definition_multi_file(self
) -> None:
888 Test hh_client --ide-get-definition when definition we look for is
889 in file different from input file
891 self
.test_driver
.start_hh_server()
893 self
.test_driver
.check_cmd_and_json_cmd(
895 "name: \\ClassToBeIdentified::methodToBeIdentified, kind: method,"
896 " span: line 1, characters 45-64, is_declaration: false",
898 " methodToBeIdentified",
900 " id: method::ClassToBeIdentified::methodToBeIdentified",
901 ' position: File "{root}foo_5.php", line 4, characters 26-45:',
902 ' span: File "{root}foo_5.php", line 4, character 3 - line 4,'
904 " modifiers: public static ",
910 '[{{"name":"\\\\ClassToBeIdentified::methodToBeIdentified",'
911 '"result_type":"method","pos":{{"filename":"","line":1,'
912 '"char_start":45,"char_end":64}},"definition_pos":'
913 '{{"filename":"{root}foo_5.php","line":4,"char_start":26,'
914 '"char_end":45}},"definition_span":{{"filename":"{root}foo_5.php",'
915 '"line_start":4,"char_start":3,"line_end":4,"char_end":50}},'
917 '"method::ClassToBeIdentified::methodToBeIdentified"}}]'
919 options
=["--ide-get-definition", "1:50"],
920 stdin
="<?hh function test() { "
921 "ClassToBeIdentified::methodToBeIdentified () }",
924 def test_abnormal_typechecker_exit_message(self
) -> None:
926 Tests that the monitor outputs a useful message when its typechecker
930 self
.test_driver
.start_hh_server()
931 monitor_logs
= self
.test_driver
.get_monitor_logs()
933 "Just started typechecker server with pid: ([0-9]+)", monitor_logs
935 self
.assertIsNotNone(m
)
936 assert m
is not None, "for mypy"
938 self
.assertIsNotNone(pid
)
939 os
.kill(int(pid
), signal
.SIGTERM
)
940 # For some reason, waitpid in the monitor after the kill signal
941 # sent above doesn't preserve ordering - maybe because they're
942 # in separate processes? Give it some time.
944 client_error
= self
.test_driver
.check_cmd(
945 expected_output
=None, assert_loaded_saved_state
=False
947 self
.assertIn("Last server killed by signal", client_error
)
949 def test_duplicate_parent(self
) -> None:
951 This checks that we handle duplicate parent classes, i.e. when Bar
952 extends Foo and there are two declarations of Foo. We want to make sure
953 that when the duplicate gets removed, we recover correctly by
954 redeclaring Bar with the remaining parent class.
956 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
960 class Foo { // also declared in foo_3.php in setUpClass
965 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_5.php"), "w") as f
:
969 class Bar extends Foo {}
971 function main(Bar $a) {
976 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php", "foo_5.php"])
977 self
.test_driver
.check_cmd(
979 "{root}foo_4.php:3:19,21: Name already bound: `Foo` (Naming[2012])",
980 " {root}foo_3.php:7:15,17: Previous definition is here",
981 "{root}foo_5.php:6:28,29: No class variable `$y` in `Bar` (Typing[4090])",
982 " {root}foo_4.php:4:31,32: Did you mean `$x` instead?",
983 " {root}foo_5.php:3:19,21: Declaration of `Bar` is here",
987 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"))
988 self
.test_driver
.check_cmd(
990 "{root}foo_5.php:6:28,29: No class variable `$y` in `Bar` (Typing[4090])",
991 " {root}foo_5.php:3:19,21: Declaration of `Bar` is here",
995 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
1004 os
.remove(os
.path
.join(self
.test_driver
.repo_dir
, "foo_3.php"))
1005 self
.test_driver
.check_cmd(["No errors!"])
1007 def test_refactor_methods(self
) -> None:
1008 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
1012 class Bar extends Foo {
1013 public function f() {}
1014 public function g() {}
1017 class Baz extends Bar {
1018 public function g() {
1024 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php"])
1026 self
.test_driver
.check_cmd_and_json_cmd(
1027 ["Rewrote 1 files."],
1029 '[{{"filename":"{root}foo_4.php","patches":[{{'
1030 '"char_start":84,"char_end":85,"line":4,"col_start":33,'
1031 '"col_end":33,"patch_type":"replace","replacement":"wat"}},'
1032 '{{"char_start":246,"char_end":247,"line":10,"col_start":28,'
1033 '"col_end":28,"patch_type":"replace","replacement":"wat"}}]}}]'
1035 options
=["--refactor", "Method", "Bar::f", "Bar::wat"],
1037 self
.test_driver
.check_cmd_and_json_cmd(
1038 ["Rewrote 1 files."],
1040 '[{{"filename":"{root}foo_4.php","patches":[{{'
1041 '"char_start":125,"char_end":126,"line":5,"col_start":33,'
1042 '"col_end":33,"patch_type":"replace",'
1043 '"replacement":"overrideMe"}},{{"char_start":215,'
1044 '"char_end":216,"line":9,"col_start":33,"col_end":33,'
1045 '"patch_type":"replace","replacement":"overrideMe"}}]}}]'
1047 options
=["--refactor", "Method", "Bar::g", "Bar::overrideMe"],
1049 self
.test_driver
.check_cmd_and_json_cmd(
1050 ["Rewrote 2 files."],
1052 '[{{"filename":"{root}foo_4.php","patches":[{{'
1053 '"char_start":46,"char_end":49,"line":3,"col_start":31,'
1054 '"col_end":33,"patch_type":"replace","replacement":"Qux"}}]}},'
1055 '{{"filename":"{root}foo_3.php","patches":[{{'
1056 '"char_start":96,"char_end":99,"line":7,"col_start":15,'
1057 '"col_end":17,"patch_type":"replace","replacement":"Qux"}},'
1058 '{{"char_start":165,"char_end":168,"line":10,"col_start":17,'
1059 '"col_end":19,"patch_type":"replace","replacement":"Qux"}}]'
1062 options
=["--refactor", "Class", "Foo", "Qux"],
1065 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php")) as f
:
1071 class Bar extends Qux {
1072 public function wat() {}
1073 public function overrideMe() {}
1076 class Baz extends Bar {
1077 public function overrideMe() {
1084 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_3.php")) as f
:
1090 function h(): string {
1096 function some_long_function_name() {
1103 def test_refactor_functions(self
) -> None:
1104 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
1116 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php"])
1118 self
.test_driver
.check_cmd_and_json_cmd(
1119 ["Rewrote 1 files."],
1121 '[{{"filename":"{root}foo_4.php","patches":[{{'
1122 '"char_start":132,"char_end":135,"line":8,"col_start":22,'
1123 '"col_end":24,"patch_type":"replace","replacement":"woah"}},'
1124 '{{"char_start":61,"char_end":64,"line":4,"col_start":17,'
1125 '"col_end":19,"patch_type":"replace","replacement":"woah"}}]'
1128 options
=["--refactor", "Function", "wat", "woah"],
1130 self
.test_driver
.check_cmd_and_json_cmd(
1131 ["Rewrote 2 files."],
1133 '[{{"filename":"{root}foo_4.php","patches":[{{'
1134 '"char_start":92,"char_end":93,"line":5,"col_start":24,'
1135 '"col_end":24,"patch_type":"replace","replacement":"fff"}}]}},'
1136 '{{"filename":"{root}foo_1.php","patches":[{{'
1137 '"char_start":33,"char_end":34,"line":3,"col_start":18,'
1138 '"col_end":18,"patch_type":"replace","replacement":"fff"}}]'
1141 options
=["--refactor", "Function", "f", "fff"],
1144 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php")) as f
:
1159 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_1.php")) as f
:
1171 def test_refactor_typedefs(self
) -> None:
1172 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php"), "w") as f
:
1176 newtype NewType = int;
1180 public function myFunc(Type $x): NewType {
1186 self
.test_driver
.start_hh_server(changed_files
=["foo_4.php"])
1188 self
.test_driver
.check_cmd_and_json_cmd(
1189 ["Rewrote 1 files."],
1191 '[{{"filename":"{root}foo_4.php","patches":[{{'
1192 '"char_start":36,"char_end":43,"line":3,"col_start":21,'
1193 '"col_end":27,"patch_type":"replace","replacement":"NewTypeX"}},'
1194 '{{"char_start":158,"char_end":165,"line":7,"col_start":50,'
1195 '"col_end":56,"patch_type":"replace","replacement":"NewTypeX"}}]'
1198 options
=["--refactor", "Class", "NewType", "NewTypeX"],
1201 self
.test_driver
.check_cmd_and_json_cmd(
1202 ["Rewrote 1 files."],
1204 '[{{"filename":"{root}foo_4.php","patches":[{{'
1205 '"char_start":69,"char_end":73,"line":4,"col_start":18,'
1206 '"col_end":21,"patch_type":"replace","replacement":"TypeX"}},'
1207 '{{"char_start":149,"char_end":153,"line":7,"col_start":40,'
1208 '"col_end":43,"patch_type":"replace","replacement":"TypeX"}}]'
1211 options
=["--refactor", "Class", "Type", "TypeX"],
1214 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "foo_4.php")) as f
:
1220 newtype NewTypeX = int;
1224 public function myFunc(TypeX $x): NewTypeX {
1231 def test_auto_namespace_alias_addition(self
) -> None:
1233 Add namespace alias and check if it is still good
1236 self
.test_driver
.start_hh_server()
1237 self
.test_driver
.check_cmd(["No errors!"])
1239 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "auto_ns_2.php"), "w") as f
:
1250 self
.test_driver
.check_cmd(["No errors!"])
1252 def test_interrupt(self
) -> None:
1253 # filesystem interruptions are only triggered by Watchman
1254 with
open(os
.path
.join(self
.test_driver
.repo_dir
, ".watchmanconfig"), "w") as f
:
1256 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "hh.conf"), "a") as f
:
1258 "use_watchman = true\n"
1259 + "interrupt_on_watchman = true\n"
1260 + "interrupt_on_client = true\n"
1261 + "watchman_subscribe_v2 = true\n"
1264 self
.test_driver
.start_hh_server()
1265 self
.test_driver
.start_hh_loop_forever_assert_timeout()
1266 self
.test_driver
.check_cmd(
1267 ["string"], options
=["--type-at-pos", "{root}foo_3.php:11:14"]
1269 self
.test_driver
.stop_hh_loop_forever()
1271 def test_status_single(self
) -> None:
1273 Test hh_client check --single
1275 self
.test_driver
.start_hh_server()
1278 os
.path
.join(self
.test_driver
.repo_dir
, "typing_error.php"), "w"
1280 f
.write("<?hh //strict\n function aaaa(): int { return h(); }")
1282 self
.test_driver
.check_cmd(
1284 "{root}typing_error.php:2:32,34: Invalid return type (Typing[4110])",
1285 " {root}typing_error.php:2:19,21: Expected `int`",
1286 " {root}foo_3.php:3:23,28: But got `string`",
1288 options
=["--single", "{root}typing_error.php"],
1292 self
.test_driver
.check_cmd(
1294 ":2:32,34: Invalid return type (Typing[4110])",
1295 " :2:19,21: Expected `int`",
1296 " {root}foo_3.php:3:23,28: But got `string`",
1298 options
=["--single", "-"],
1299 stdin
="<?hh //strict\n function aaaa(): int { return h(); }",
1302 def test_lint_xcontroller(self
) -> None:
1303 self
.test_driver
.start_hh_server()
1305 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "in_list.txt"), "w") as f
:
1306 f
.write(os
.path
.join(self
.test_driver
.repo_dir
, "xcontroller.php"))
1308 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "xcontroller.php"), "w") as f
:
1310 "<?hh\n class MyXController extends XControllerBase { "
1311 "public function getPath() { return f(); } }"
1314 self
.test_driver
.check_cmd(
1316 'File "{root}xcontroller.php", line 2, characters 8-20:',
1317 "When linting MyXController: The body of isDelegateOnly should "
1318 "only contain `return true;` or `return false;` (Lint[5615])",
1319 'File "{root}xcontroller.php", line 2, characters 8-20:',
1320 "When linting MyXController: getPath method of MyXController must "
1321 "be present and return a static literal for build purposes (Lint[5615])",
1323 options
=["--lint-xcontroller", "{root}in_list.txt"],
1326 def test_incremental_typecheck_same_file(self
) -> None:
1328 self
.test_driver
.start_hh_server()
1330 # Important: typecheck the file after creation but before adding contents
1331 # to test forward naming table updating.
1334 self
.test_driver
.repo_dir
, "test_incremental_typecheck_same_file.php"
1338 self
.test_driver
.check_cmd(["No errors!"])
1342 self
.test_driver
.repo_dir
, "test_incremental_typecheck_same_file.php"
1349 // test_incremental_typecheck_same_file
1350 class TestIncrementalTypecheckSameFile {}
1353 self
.test_driver
.check_cmd(["No errors!"])
1355 # Notice how the only change is the removed doc block.
1358 self
.test_driver
.repo_dir
, "test_incremental_typecheck_same_file.php"
1365 class TestIncrementalTypecheckSameFile {}
1368 self
.test_driver
.check_cmd(["No errors!"])