Backout D24132229
[hiphop-php.git] / hphp / hack / test / integration / test_save_state.py
blob2e088b46ae150e632645c584016883f8c4b34098
1 # pyre-strict
3 from __future__ import absolute_import, division, print_function, unicode_literals
5 import json
6 import os
7 import shlex
8 import shutil
9 import sqlite3
10 import stat
11 import time
12 import unittest
13 from typing import Optional, TextIO
15 import common_tests
16 import hierarchy_tests
17 from hh_paths import hh_client
18 from saved_state_test_driver import (
19 SavedStateClassicTestDriver,
20 SavedStateTestDriver,
21 SaveStateResult,
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:
33 f.write(
34 r"""
35 # some comment
36 use_mini_state = true
37 use_watchman = true
38 watchman_subscribe_v2 = true
39 lazy_decl = true
40 lazy_parse = true
41 lazy_init2 = true
42 incremental_init = true
43 enable_fuzzy_search = false
44 max_workers = 2
45 """
49 class LazyInitCommonTests(common_tests.CommonTests):
50 @classmethod
51 def get_test_driver(cls) -> LazyInitTestDriver:
52 return LazyInitTestDriver()
55 class LazyInitHeirarchyTests(hierarchy_tests.HierarchyTests):
56 @classmethod
57 def get_test_driver(cls) -> LazyInitTestDriver:
58 return LazyInitTestDriver()
61 class SavedStateCommonTests(common_tests.CommonTests):
62 @classmethod
63 def get_test_driver(cls) -> SavedStateTestDriver:
64 return SavedStateTestDriver()
67 class SavedStateBarebonesTestsClassic(common_tests.BarebonesTests):
68 @classmethod
69 def get_test_driver(cls) -> SavedStateClassicTestDriver:
70 return SavedStateClassicTestDriver()
73 class SavedStateHierarchyTests(hierarchy_tests.HierarchyTests):
74 @classmethod
75 def get_test_driver(cls) -> SavedStateTestDriver:
76 return SavedStateTestDriver()
79 class SavedStateTests(TestCase[SavedStateTestDriver]):
80 @classmethod
81 def get_test_driver(cls) -> SavedStateTestDriver:
82 return SavedStateTestDriver()
84 def test_hhconfig_change(self) -> None:
85 """
86 Start hh_server, then change .hhconfig and check that the server
87 restarts itself
88 """
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:
92 f.write(
93 r"""
94 # some comment
95 assume_php = true
96 """
99 # Server may take some time to kill itself.
100 time.sleep(2)
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
115 log_file = (
116 self.test_driver.proc_call(
117 [hh_client, "--logname", self.test_driver.repo_dir]
118 )[0].strip()
119 + ".old"
121 with open(log_file) as f:
122 logs = f.read()
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:
127 f.write(
128 r"""
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"
149 self.assertTrue(
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,
167 filename="partial4",
169 assert (
170 result4.returned_values.get_edges_added()
171 == result3.returned_values.get_edges_added()
174 result5 = self.test_driver.save_partial(
175 files_to_check=[
176 {"from_prefix_incl": "class_1.php", "to_prefix_excl": "class_3.php"}
178 assert_edges_added=True,
179 filename="partial5",
181 assert (
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(
304 ignore_errors=True
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
335 # from the change.
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(
349 ignore_errors=True
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"),
357 type="int",
358 value="11",
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:
370 # Save state
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)
376 self.assertEqual(
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
386 self.assertEqual(
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)
397 # Make a change
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
407 assert (
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:
416 f.write(
417 """<?hh // strict
419 class UsesAToo {
420 public function test() : int {
421 return A::foo();
428 def change_return_type_on_base_class(
429 self, filename: str, type: str = "string", value: str = '"Hello"'
430 ) -> None:
431 # Change the return type
432 with open(filename, "w") as f:
433 f.write(
434 """<?hh // strict
436 class B {
438 public static function foo () : %s {
439 return %s;
443 % (type, value)
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:
452 f.write(
453 r"""
454 # some comment
455 use_mini_state = true
456 use_watchman = true
457 watchman_subscribe_v2 = true
458 lazy_decl = true
459 lazy_parse = true
460 lazy_init2 = true
461 enable_naming_table_fallback = true
466 class ReverseNamingTableSavedStateCommonTests(common_tests.CommonTests):
467 @classmethod
468 def get_test_driver(cls) -> ReverseNamingTableFallbackTestDriver:
469 return ReverseNamingTableFallbackTestDriver()
472 class ReverseNamingTableSavedStateHierarchyTests(hierarchy_tests.HierarchyTests):
473 @classmethod
474 def get_test_driver(cls) -> ReverseNamingTableFallbackTestDriver:
475 return ReverseNamingTableFallbackTestDriver()
478 class ReverseNamingTableSavedStateTests(SavedStateTests):
479 @classmethod
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(
497 changed_files=[],
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)