3 from __future__
import absolute_import
, division
, print_function
, unicode_literals
13 from typing
import Optional
, TextIO
16 import hierarchy_tests
17 from hh_paths
import hh_client
18 from saved_state_test_driver
import (
19 SavedStateClassicTestDriver
,
23 from test_case
import TestCase
26 def write_echo_json(f
: TextIO
, obj
: object) -> None:
27 f
.write("echo %s\n" % shlex
.quote(json
.dumps(obj
)))
30 class LazyInitTestDriver(SavedStateTestDriver
):
31 def write_local_conf(self
) -> None:
32 with
open(os
.path
.join(self
.repo_dir
, "hh.conf"), "w") as f
:
38 watchman_subscribe_v2 = true
42 incremental_init = true
43 enable_fuzzy_search = false
49 class LazyInitCommonTests(common_tests
.CommonTests
):
51 def get_test_driver(cls
) -> LazyInitTestDriver
:
52 return LazyInitTestDriver()
55 class LazyInitHeirarchyTests(hierarchy_tests
.HierarchyTests
):
57 def get_test_driver(cls
) -> LazyInitTestDriver
:
58 return LazyInitTestDriver()
61 class SavedStateCommonTests(common_tests
.CommonTests
):
63 def get_test_driver(cls
) -> SavedStateTestDriver
:
64 return SavedStateTestDriver()
67 class SavedStateBarebonesTestsClassic(common_tests
.BarebonesTests
):
69 def get_test_driver(cls
) -> SavedStateClassicTestDriver
:
70 return SavedStateClassicTestDriver()
73 class SavedStateHierarchyTests(hierarchy_tests
.HierarchyTests
):
75 def get_test_driver(cls
) -> SavedStateTestDriver
:
76 return SavedStateTestDriver()
79 class SavedStateTests(TestCase
[SavedStateTestDriver
]):
81 def get_test_driver(cls
) -> SavedStateTestDriver
:
82 return SavedStateTestDriver()
84 def test_hhconfig_change(self
) -> None:
86 Start hh_server, then change .hhconfig and check that the server
89 self
.test_driver
.start_hh_server()
90 self
.test_driver
.check_cmd(["No errors!"])
91 with
open(os
.path
.join(self
.test_driver
.repo_dir
, ".hhconfig"), "w") as f
:
99 # Server may take some time to kill itself.
102 # The sleep(2) above also almost-always ensures another race condition
103 # goes the way we want: The informant-directed restart doesn't happen
104 # *during* processing of a new client connection. The ambiguity of that
105 # situation (whether or not the newly-connected client did read the
106 # new hhconfig file contents or not) means that the Monitor can't safely
107 # start a new server instance until the *next* client connects. Just in
108 # case the race doesn't go the way we want, add another "check_cmd"
109 # call here to force the Monitor into the state we want.
110 self
.test_driver
.check_cmd(None, assert_loaded_saved_state
=False)
112 # this should start a new server
113 self
.test_driver
.check_cmd(["No errors!"])
114 # check how the old one exited
116 self
.test_driver
.proc_call(
117 [hh_client
, "--logname", self
.test_driver
.repo_dir
]
121 with
open(log_file
) as f
:
123 self
.assertIn(".hhconfig changed in an incompatible way", logs
)
125 def test_watchman_timeout(self
) -> None:
126 with
open(os
.path
.join(self
.test_driver
.repo_dir
, "hh.conf"), "a") as f
:
129 watchman_init_timeout = 1
133 with
open(os
.path
.join(self
.test_driver
.bin_dir
, "watchman"), "w") as f
:
134 f
.write(r
"""sleep 2""")
135 os
.fchmod(f
.fileno(), stat
.S_IRWXU
)
137 self
.test_driver
.run_check()
138 # Stop the server, ensuring that its logs get flushed
139 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
140 self
.assertIn("Watchman_sig.Types.Timeout", self
.test_driver
.get_server_logs())
142 def test_save_partial_state(self
) -> None:
143 self
.test_driver
.start_hh_server()
145 result1
= self
.test_driver
.save_partial(
146 files_to_check
=["class_1.php"], assert_edges_added
=True, filename
="partial1"
150 result1
.returned_values
.get_edges_added() == 0,
151 "class_1 has no dependencies",
154 result2
= self
.test_driver
.save_partial(
155 files_to_check
=["class_2.php"], assert_edges_added
=True, filename
="partial2"
157 assert result2
.returned_values
.get_edges_added() > 0
159 result3
= self
.test_driver
.save_partial(
160 files_to_check
=["class_3.php"], assert_edges_added
=True, filename
="partial3"
162 assert result3
.returned_values
.get_edges_added() > 0
164 result4
= self
.test_driver
.save_partial(
165 files_to_check
=["class_1.php", "class_2.php", "class_3.php"],
166 assert_edges_added
=True,
170 result4
.returned_values
.get_edges_added()
171 == result3
.returned_values
.get_edges_added()
174 result5
= self
.test_driver
.save_partial(
176 {"from_prefix_incl": "class_1.php", "to_prefix_excl": "class_3.php"}
178 assert_edges_added
=True,
182 result5
.returned_values
.get_edges_added()
183 == result2
.returned_values
.get_edges_added()
186 def test_incrementally_generated_saved_state(self
) -> None:
187 old_saved_state
: SaveStateResult
= self
.test_driver
.dump_saved_state()
188 new_file
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3b.php")
189 self
.add_file_that_depends_on_class_a(new_file
)
190 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=False)
191 new_saved_state
: SaveStateResult
= self
.test_driver
.dump_saved_state(
192 assert_edges_added
=True
194 assert new_saved_state
.returned_values
.get_edges_added() > 0
196 self
.change_return_type_on_base_class(
197 os
.path
.join(self
.test_driver
.repo_dir
, "class_1.php")
199 self
.test_driver
.check_cmd(
201 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
202 " {root}class_3.php:4:28,30: Expected `int`",
203 " {root}class_1.php:5:33,38: But got `string`",
204 "{root}class_3b.php:5:8,15: Invalid return type (Typing[4110])",
205 " {root}class_3b.php:4:26,28: Expected `int`",
206 " {root}class_1.php:5:33,38: But got `string`",
208 assert_loaded_saved_state
=False,
210 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
211 # Start server with the original saved state. Will be missing the
212 # second error because of the missing edge.
213 self
.test_driver
.start_hh_server(
214 changed_files
=["class_1.php"], saved_state_path
=old_saved_state
.path
216 self
.test_driver
.check_cmd(
218 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
219 " {root}class_3.php:4:28,30: Expected `int`",
220 " {root}class_1.php:5:33,38: But got `string`",
223 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
224 # Start another server with the new saved state. Will have both errors.
225 self
.test_driver
.start_hh_server(
226 changed_files
=["class_1.php"], saved_state_path
=new_saved_state
.path
228 self
.test_driver
.check_cmd(
230 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
231 " {root}class_3.php:4:28,30: Expected `int`",
232 " {root}class_1.php:5:33,38: But got `string`",
233 "{root}class_3b.php:5:8,15: Invalid return type (Typing[4110])",
234 " {root}class_3b.php:4:26,28: Expected `int`",
235 " {root}class_1.php:5:33,38: But got `string`",
239 def test_incrementally_generated_saved_state_after_loaded_saved_state(self
) -> None:
240 # Same as the above test, except we begin the test by starting up
241 # a Hack Server that loads a saved state.
242 self
.test_driver
.start_hh_server()
243 # Hack server is now started with a saved state
244 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=True)
245 old_saved_state
= self
.test_driver
.dump_saved_state()
247 new_file
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3b.php")
248 self
.add_file_that_depends_on_class_a(new_file
)
249 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=True)
250 new_saved_state
= self
.test_driver
.dump_saved_state(assert_edges_added
=True)
252 assert new_saved_state
.returned_values
.get_edges_added() > 0
254 self
.change_return_type_on_base_class(
255 os
.path
.join(self
.test_driver
.repo_dir
, "class_1.php")
257 self
.test_driver
.check_cmd(
259 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
260 " {root}class_3.php:4:28,30: Expected `int`",
261 " {root}class_1.php:5:33,38: But got `string`",
262 "{root}class_3b.php:5:8,15: Invalid return type (Typing[4110])",
263 " {root}class_3b.php:4:26,28: Expected `int`",
264 " {root}class_1.php:5:33,38: But got `string`",
266 assert_loaded_saved_state
=True,
268 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
269 # Start server with the original saved state. Will be missing the
270 # second error because of the missing edge.
271 self
.test_driver
.start_hh_server(
272 changed_files
=["class_1.php"], saved_state_path
=old_saved_state
.path
274 self
.test_driver
.check_cmd(
276 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
277 " {root}class_3.php:4:28,30: Expected `int`",
278 " {root}class_1.php:5:33,38: But got `string`",
281 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
282 # Start another server with the new saved state. Will have both errors.
283 self
.test_driver
.start_hh_server(
284 changed_files
=["class_1.php"], saved_state_path
=new_saved_state
.path
286 self
.test_driver
.check_cmd(
288 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
289 " {root}class_3.php:4:28,30: Expected `int`",
290 " {root}class_1.php:5:33,38: But got `string`",
291 "{root}class_3b.php:5:8,15: Invalid return type (Typing[4110])",
292 " {root}class_3b.php:4:26,28: Expected `int`",
293 " {root}class_1.php:5:33,38: But got `string`",
297 def test_incrementally_generated_saved_state_with_errors(self
) -> None:
298 # Introduce an error in "master"
299 self
.change_return_type_on_base_class(
300 os
.path
.join(self
.test_driver
.repo_dir
, "class_1.php")
303 saved_state_with_1_error
: SaveStateResult
= self
.test_driver
.dump_saved_state(
307 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
309 # Start server with the saved state, assume there are no local changes.
310 self
.test_driver
.start_hh_server(
311 changed_files
=None, saved_state_path
=saved_state_with_1_error
.path
314 # We still expect that the error from the saved state shows up.
315 self
.test_driver
.check_cmd(
317 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
318 " {root}class_3.php:4:28,30: Expected `int`",
319 " {root}class_1.php:5:33,38: But got `string`",
323 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
325 new_file
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3b.php")
326 self
.add_file_that_depends_on_class_a(new_file
)
328 # Start server with the saved state, the only change is in the new file.
329 self
.test_driver
.start_hh_server(
330 changed_files
=["class_3b.php"],
331 saved_state_path
=saved_state_with_1_error
.path
,
334 # Now we expect 2 errors - one from the saved state and one
336 self
.test_driver
.check_cmd(
338 "{root}class_3.php:5:12,19: Invalid return type (Typing[4110])",
339 " {root}class_3.php:4:28,30: Expected `int`",
340 " {root}class_1.php:5:33,38: But got `string`",
341 "{root}class_3b.php:5:8,15: Invalid return type (Typing[4110])",
342 " {root}class_3b.php:4:26,28: Expected `int`",
343 " {root}class_1.php:5:33,38: But got `string`",
345 assert_loaded_saved_state
=False,
348 saved_state_with_2_errors
= self
.test_driver
.dump_saved_state(
352 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
354 # Let's fix the error
355 self
.change_return_type_on_base_class(
356 filename
=os
.path
.join(self
.test_driver
.repo_dir
, "class_1.php"),
361 # Start another server with the new saved state. Will have both errors.
362 self
.test_driver
.start_hh_server(
363 changed_files
=["class_1.php"],
364 saved_state_path
=saved_state_with_2_errors
.path
,
367 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=True)
369 def test_replace_state_after_saving(self
) -> None:
371 result
= self
.test_driver
.dump_saved_state(assert_edges_added
=True)
372 assert result
.returned_values
.get_edges_added() > 0
374 # Save state again - confirm the same number of edges is dumped
375 result2
= self
.test_driver
.dump_saved_state(assert_edges_added
=True)
377 result
.returned_values
.get_edges_added(),
378 result2
.returned_values
.get_edges_added(),
381 # Save state with the 'replace' arg
382 replace_result1
= self
.test_driver
.dump_saved_state(
383 assert_edges_added
=True, replace_state_after_saving
=True
387 result
.returned_values
.get_edges_added(),
388 replace_result1
.returned_values
.get_edges_added(),
391 # Save state with the new arg - confirm there are 0 new edges
392 replace_result2
= self
.test_driver
.dump_saved_state(
393 assert_edges_added
=True, replace_state_after_saving
=True
395 self
.assertEqual(replace_result2
.returned_values
.get_edges_added(), 0)
398 # Save state - confirm there are only the # of new edges
399 # corresponding to the one change
400 new_file
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3b.php")
401 self
.add_file_that_depends_on_class_a(new_file
)
402 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=False)
403 replace_incremental
= self
.test_driver
.dump_saved_state(
404 assert_edges_added
=True, replace_state_after_saving
=True
408 replace_incremental
.returned_values
.get_edges_added()
409 < result
.returned_values
.get_edges_added()
411 assert replace_incremental
.returned_values
.get_edges_added() > 0
412 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=False)
414 def add_file_that_depends_on_class_a(self
, filename
: str) -> None:
415 with
open(filename
, "w") as f
:
420 public function test() : int {
428 def change_return_type_on_base_class(
429 self
, filename
: str, type: str = "string", value
: str = '"Hello"'
431 # Change the return type
432 with
open(filename
, "w") as f
:
438 public static function foo () : %s {
447 class ReverseNamingTableFallbackTestDriver(SavedStateTestDriver
):
448 enable_naming_table_fallback
= True
450 def write_local_conf(self
) -> None:
451 with
open(os
.path
.join(self
.repo_dir
, "hh.conf"), "w") as f
:
455 use_mini_state = true
457 watchman_subscribe_v2 = true
461 enable_naming_table_fallback = true
466 class ReverseNamingTableSavedStateCommonTests(common_tests
.CommonTests
):
468 def get_test_driver(cls
) -> ReverseNamingTableFallbackTestDriver
:
469 return ReverseNamingTableFallbackTestDriver()
472 class ReverseNamingTableSavedStateHierarchyTests(hierarchy_tests
.HierarchyTests
):
474 def get_test_driver(cls
) -> ReverseNamingTableFallbackTestDriver
:
475 return ReverseNamingTableFallbackTestDriver()
478 class ReverseNamingTableSavedStateTests(SavedStateTests
):
480 def get_test_driver(cls
) -> ReverseNamingTableFallbackTestDriver
:
481 return ReverseNamingTableFallbackTestDriver()
483 def test_file_moved(self
) -> None:
484 new_file
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3b.php")
485 self
.add_file_that_depends_on_class_a(new_file
)
486 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=False)
487 naming_table_path
= self
.test_driver
.dump_naming_saved_state(
488 self
.test_driver
.repo_dir
,
489 saved_state_path
=os
.path
.join(self
.test_driver
.repo_dir
, "new"),
492 self
.test_driver
.proc_call([hh_client
, "stop", self
.test_driver
.repo_dir
])
493 new_file2
= os
.path
.join(self
.test_driver
.repo_dir
, "class_3c.php")
494 shutil
.move(new_file
, new_file2
)
496 self
.test_driver
.start_hh_server(
498 changed_naming_files
=["class_3c.php"],
499 naming_saved_state_path
=naming_table_path
,
501 self
.test_driver
.check_cmd(["No errors!"], assert_loaded_saved_state
=True)