test_autoreload.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817
  1. import contextlib
  2. import os
  3. import py_compile
  4. import shutil
  5. import sys
  6. import tempfile
  7. import threading
  8. import time
  9. import types
  10. import weakref
  11. import zipfile
  12. from importlib import import_module
  13. from pathlib import Path
  14. from subprocess import CompletedProcess
  15. from unittest import mock, skip, skipIf
  16. try:
  17. import zoneinfo
  18. except ImportError:
  19. from backports import zoneinfo
  20. import django.__main__
  21. from django.apps.registry import Apps
  22. from django.test import SimpleTestCase
  23. from django.test.utils import extend_sys_path
  24. from django.utils import autoreload
  25. from django.utils.autoreload import WatchmanUnavailable
  26. from .test_module import __main__ as test_main, main_module as test_main_module
  27. from .utils import on_macos_with_hfs
  28. class TestIterModulesAndFiles(SimpleTestCase):
  29. def import_and_cleanup(self, name):
  30. import_module(name)
  31. self.addCleanup(lambda: sys.path_importer_cache.clear())
  32. self.addCleanup(lambda: sys.modules.pop(name, None))
  33. def clear_autoreload_caches(self):
  34. autoreload.iter_modules_and_files.cache_clear()
  35. def assertFileFound(self, filename):
  36. # Some temp directories are symlinks. Python resolves these fully while
  37. # importing.
  38. resolved_filename = filename.resolve(strict=True)
  39. self.clear_autoreload_caches()
  40. # Test uncached access
  41. self.assertIn(resolved_filename, list(autoreload.iter_all_python_module_files()))
  42. # Test cached access
  43. self.assertIn(resolved_filename, list(autoreload.iter_all_python_module_files()))
  44. self.assertEqual(autoreload.iter_modules_and_files.cache_info().hits, 1)
  45. def assertFileNotFound(self, filename):
  46. resolved_filename = filename.resolve(strict=True)
  47. self.clear_autoreload_caches()
  48. # Test uncached access
  49. self.assertNotIn(resolved_filename, list(autoreload.iter_all_python_module_files()))
  50. # Test cached access
  51. self.assertNotIn(resolved_filename, list(autoreload.iter_all_python_module_files()))
  52. self.assertEqual(autoreload.iter_modules_and_files.cache_info().hits, 1)
  53. def temporary_file(self, filename):
  54. dirname = tempfile.mkdtemp()
  55. self.addCleanup(shutil.rmtree, dirname)
  56. return Path(dirname) / filename
  57. def test_paths_are_pathlib_instances(self):
  58. for filename in autoreload.iter_all_python_module_files():
  59. self.assertIsInstance(filename, Path)
  60. def test_file_added(self):
  61. """
  62. When a file is added, it's returned by iter_all_python_module_files().
  63. """
  64. filename = self.temporary_file('test_deleted_removed_module.py')
  65. filename.touch()
  66. with extend_sys_path(str(filename.parent)):
  67. self.import_and_cleanup('test_deleted_removed_module')
  68. self.assertFileFound(filename.absolute())
  69. def test_check_errors(self):
  70. """
  71. When a file containing an error is imported in a function wrapped by
  72. check_errors(), gen_filenames() returns it.
  73. """
  74. filename = self.temporary_file('test_syntax_error.py')
  75. filename.write_text("Ceci n'est pas du Python.")
  76. with extend_sys_path(str(filename.parent)):
  77. try:
  78. with self.assertRaises(SyntaxError):
  79. autoreload.check_errors(import_module)('test_syntax_error')
  80. finally:
  81. autoreload._exception = None
  82. self.assertFileFound(filename)
  83. def test_check_errors_catches_all_exceptions(self):
  84. """
  85. Since Python may raise arbitrary exceptions when importing code,
  86. check_errors() must catch Exception, not just some subclasses.
  87. """
  88. filename = self.temporary_file('test_exception.py')
  89. filename.write_text('raise Exception')
  90. with extend_sys_path(str(filename.parent)):
  91. try:
  92. with self.assertRaises(Exception):
  93. autoreload.check_errors(import_module)('test_exception')
  94. finally:
  95. autoreload._exception = None
  96. self.assertFileFound(filename)
  97. def test_zip_reload(self):
  98. """
  99. Modules imported from zipped files have their archive location included
  100. in the result.
  101. """
  102. zip_file = self.temporary_file('zip_import.zip')
  103. with zipfile.ZipFile(str(zip_file), 'w', zipfile.ZIP_DEFLATED) as zipf:
  104. zipf.writestr('test_zipped_file.py', '')
  105. with extend_sys_path(str(zip_file)):
  106. self.import_and_cleanup('test_zipped_file')
  107. self.assertFileFound(zip_file)
  108. def test_bytecode_conversion_to_source(self):
  109. """.pyc and .pyo files are included in the files list."""
  110. filename = self.temporary_file('test_compiled.py')
  111. filename.touch()
  112. compiled_file = Path(py_compile.compile(str(filename), str(filename.with_suffix('.pyc'))))
  113. filename.unlink()
  114. with extend_sys_path(str(compiled_file.parent)):
  115. self.import_and_cleanup('test_compiled')
  116. self.assertFileFound(compiled_file)
  117. def test_weakref_in_sys_module(self):
  118. """iter_all_python_module_file() ignores weakref modules."""
  119. time_proxy = weakref.proxy(time)
  120. sys.modules['time_proxy'] = time_proxy
  121. self.addCleanup(lambda: sys.modules.pop('time_proxy', None))
  122. list(autoreload.iter_all_python_module_files()) # No crash.
  123. def test_module_without_spec(self):
  124. module = types.ModuleType('test_module')
  125. del module.__spec__
  126. self.assertEqual(autoreload.iter_modules_and_files((module,), frozenset()), frozenset())
  127. def test_main_module_is_resolved(self):
  128. main_module = sys.modules['__main__']
  129. self.assertFileFound(Path(main_module.__file__))
  130. def test_main_module_without_file_is_not_resolved(self):
  131. fake_main = types.ModuleType('__main__')
  132. self.assertEqual(autoreload.iter_modules_and_files((fake_main,), frozenset()), frozenset())
  133. def test_path_with_embedded_null_bytes(self):
  134. for path in (
  135. 'embedded_null_byte\x00.py',
  136. 'di\x00rectory/embedded_null_byte.py',
  137. ):
  138. with self.subTest(path=path):
  139. self.assertEqual(
  140. autoreload.iter_modules_and_files((), frozenset([path])),
  141. frozenset(),
  142. )
  143. class TestChildArguments(SimpleTestCase):
  144. @mock.patch.dict(sys.modules, {'__main__': django.__main__})
  145. @mock.patch('sys.argv', [django.__main__.__file__, 'runserver'])
  146. @mock.patch('sys.warnoptions', [])
  147. def test_run_as_module(self):
  148. self.assertEqual(
  149. autoreload.get_child_arguments(),
  150. [sys.executable, '-m', 'django', 'runserver']
  151. )
  152. @mock.patch.dict(sys.modules, {'__main__': test_main})
  153. @mock.patch('sys.argv', [test_main.__file__, 'runserver'])
  154. @mock.patch('sys.warnoptions', [])
  155. def test_run_as_non_django_module(self):
  156. self.assertEqual(
  157. autoreload.get_child_arguments(),
  158. [sys.executable, '-m', 'utils_tests.test_module', 'runserver'],
  159. )
  160. @mock.patch.dict(sys.modules, {'__main__': test_main_module})
  161. @mock.patch('sys.argv', [test_main.__file__, 'runserver'])
  162. @mock.patch('sys.warnoptions', [])
  163. def test_run_as_non_django_module_non_package(self):
  164. self.assertEqual(
  165. autoreload.get_child_arguments(),
  166. [sys.executable, '-m', 'utils_tests.test_module.main_module', 'runserver'],
  167. )
  168. @mock.patch('__main__.__spec__', None)
  169. @mock.patch('sys.argv', [__file__, 'runserver'])
  170. @mock.patch('sys.warnoptions', ['error'])
  171. def test_warnoptions(self):
  172. self.assertEqual(
  173. autoreload.get_child_arguments(),
  174. [sys.executable, '-Werror', __file__, 'runserver']
  175. )
  176. @mock.patch('__main__.__spec__', None)
  177. @mock.patch('sys.warnoptions', [])
  178. def test_exe_fallback(self):
  179. with tempfile.TemporaryDirectory() as tmpdir:
  180. exe_path = Path(tmpdir) / 'django-admin.exe'
  181. exe_path.touch()
  182. with mock.patch('sys.argv', [exe_path.with_suffix(''), 'runserver']):
  183. self.assertEqual(
  184. autoreload.get_child_arguments(),
  185. [exe_path, 'runserver']
  186. )
  187. @mock.patch('__main__.__spec__', None)
  188. @mock.patch('sys.warnoptions', [])
  189. def test_entrypoint_fallback(self):
  190. with tempfile.TemporaryDirectory() as tmpdir:
  191. script_path = Path(tmpdir) / 'django-admin-script.py'
  192. script_path.touch()
  193. with mock.patch('sys.argv', [script_path.with_name('django-admin'), 'runserver']):
  194. self.assertEqual(
  195. autoreload.get_child_arguments(),
  196. [sys.executable, script_path, 'runserver']
  197. )
  198. @mock.patch('__main__.__spec__', None)
  199. @mock.patch('sys.argv', ['does-not-exist', 'runserver'])
  200. @mock.patch('sys.warnoptions', [])
  201. def test_raises_runtimeerror(self):
  202. msg = 'Script does-not-exist does not exist.'
  203. with self.assertRaisesMessage(RuntimeError, msg):
  204. autoreload.get_child_arguments()
  205. @mock.patch('sys.argv', [__file__, 'runserver'])
  206. @mock.patch('sys.warnoptions', [])
  207. def test_module_no_spec(self):
  208. module = types.ModuleType('test_module')
  209. del module.__spec__
  210. with mock.patch.dict(sys.modules, {'__main__': module}):
  211. self.assertEqual(
  212. autoreload.get_child_arguments(),
  213. [sys.executable, __file__, 'runserver']
  214. )
  215. class TestUtilities(SimpleTestCase):
  216. def test_is_django_module(self):
  217. for module, expected in (
  218. (zoneinfo, False),
  219. (sys, False),
  220. (autoreload, True)
  221. ):
  222. with self.subTest(module=module):
  223. self.assertIs(autoreload.is_django_module(module), expected)
  224. def test_is_django_path(self):
  225. for module, expected in (
  226. (zoneinfo.__file__, False),
  227. (contextlib.__file__, False),
  228. (autoreload.__file__, True)
  229. ):
  230. with self.subTest(module=module):
  231. self.assertIs(autoreload.is_django_path(module), expected)
  232. class TestCommonRoots(SimpleTestCase):
  233. def test_common_roots(self):
  234. paths = (
  235. Path('/first/second'),
  236. Path('/first/second/third'),
  237. Path('/first/'),
  238. Path('/root/first/'),
  239. )
  240. results = autoreload.common_roots(paths)
  241. self.assertCountEqual(results, [Path('/first/'), Path('/root/first/')])
  242. class TestSysPathDirectories(SimpleTestCase):
  243. def setUp(self):
  244. self._directory = tempfile.TemporaryDirectory()
  245. self.directory = Path(self._directory.name).resolve(strict=True).absolute()
  246. self.file = self.directory / 'test'
  247. self.file.touch()
  248. def tearDown(self):
  249. self._directory.cleanup()
  250. def test_sys_paths_with_directories(self):
  251. with extend_sys_path(str(self.file)):
  252. paths = list(autoreload.sys_path_directories())
  253. self.assertIn(self.file.parent, paths)
  254. def test_sys_paths_non_existing(self):
  255. nonexistent_file = Path(self.directory.name) / 'does_not_exist'
  256. with extend_sys_path(str(nonexistent_file)):
  257. paths = list(autoreload.sys_path_directories())
  258. self.assertNotIn(nonexistent_file, paths)
  259. self.assertNotIn(nonexistent_file.parent, paths)
  260. def test_sys_paths_absolute(self):
  261. paths = list(autoreload.sys_path_directories())
  262. self.assertTrue(all(p.is_absolute() for p in paths))
  263. def test_sys_paths_directories(self):
  264. with extend_sys_path(str(self.directory)):
  265. paths = list(autoreload.sys_path_directories())
  266. self.assertIn(self.directory, paths)
  267. class GetReloaderTests(SimpleTestCase):
  268. @mock.patch('django.utils.autoreload.WatchmanReloader')
  269. def test_watchman_unavailable(self, mocked_watchman):
  270. mocked_watchman.check_availability.side_effect = WatchmanUnavailable
  271. self.assertIsInstance(autoreload.get_reloader(), autoreload.StatReloader)
  272. @mock.patch.object(autoreload.WatchmanReloader, 'check_availability')
  273. def test_watchman_available(self, mocked_available):
  274. # If WatchmanUnavailable isn't raised, Watchman will be chosen.
  275. mocked_available.return_value = None
  276. result = autoreload.get_reloader()
  277. self.assertIsInstance(result, autoreload.WatchmanReloader)
  278. class RunWithReloaderTests(SimpleTestCase):
  279. @mock.patch.dict(os.environ, {autoreload.DJANGO_AUTORELOAD_ENV: 'true'})
  280. @mock.patch('django.utils.autoreload.get_reloader')
  281. def test_swallows_keyboard_interrupt(self, mocked_get_reloader):
  282. mocked_get_reloader.side_effect = KeyboardInterrupt()
  283. autoreload.run_with_reloader(lambda: None) # No exception
  284. @mock.patch.dict(os.environ, {autoreload.DJANGO_AUTORELOAD_ENV: 'false'})
  285. @mock.patch('django.utils.autoreload.restart_with_reloader')
  286. def test_calls_sys_exit(self, mocked_restart_reloader):
  287. mocked_restart_reloader.return_value = 1
  288. with self.assertRaises(SystemExit) as exc:
  289. autoreload.run_with_reloader(lambda: None)
  290. self.assertEqual(exc.exception.code, 1)
  291. @mock.patch.dict(os.environ, {autoreload.DJANGO_AUTORELOAD_ENV: 'true'})
  292. @mock.patch('django.utils.autoreload.start_django')
  293. @mock.patch('django.utils.autoreload.get_reloader')
  294. def test_calls_start_django(self, mocked_reloader, mocked_start_django):
  295. mocked_reloader.return_value = mock.sentinel.RELOADER
  296. autoreload.run_with_reloader(mock.sentinel.METHOD)
  297. self.assertEqual(mocked_start_django.call_count, 1)
  298. self.assertSequenceEqual(
  299. mocked_start_django.call_args[0],
  300. [mock.sentinel.RELOADER, mock.sentinel.METHOD]
  301. )
  302. class StartDjangoTests(SimpleTestCase):
  303. @mock.patch('django.utils.autoreload.StatReloader')
  304. def test_watchman_becomes_unavailable(self, mocked_stat):
  305. mocked_stat.should_stop.return_value = True
  306. fake_reloader = mock.MagicMock()
  307. fake_reloader.should_stop = False
  308. fake_reloader.run.side_effect = autoreload.WatchmanUnavailable()
  309. autoreload.start_django(fake_reloader, lambda: None)
  310. self.assertEqual(mocked_stat.call_count, 1)
  311. @mock.patch('django.utils.autoreload.ensure_echo_on')
  312. def test_echo_on_called(self, mocked_echo):
  313. fake_reloader = mock.MagicMock()
  314. autoreload.start_django(fake_reloader, lambda: None)
  315. self.assertEqual(mocked_echo.call_count, 1)
  316. @mock.patch('django.utils.autoreload.check_errors')
  317. def test_check_errors_called(self, mocked_check_errors):
  318. fake_method = mock.MagicMock(return_value=None)
  319. fake_reloader = mock.MagicMock()
  320. autoreload.start_django(fake_reloader, fake_method)
  321. self.assertCountEqual(mocked_check_errors.call_args[0], [fake_method])
  322. @mock.patch('threading.Thread')
  323. @mock.patch('django.utils.autoreload.check_errors')
  324. def test_starts_thread_with_args(self, mocked_check_errors, mocked_thread):
  325. fake_reloader = mock.MagicMock()
  326. fake_main_func = mock.MagicMock()
  327. fake_thread = mock.MagicMock()
  328. mocked_check_errors.return_value = fake_main_func
  329. mocked_thread.return_value = fake_thread
  330. autoreload.start_django(fake_reloader, fake_main_func, 123, abc=123)
  331. self.assertEqual(mocked_thread.call_count, 1)
  332. self.assertEqual(
  333. mocked_thread.call_args[1],
  334. {'target': fake_main_func, 'args': (123,), 'kwargs': {'abc': 123}, 'name': 'django-main-thread'}
  335. )
  336. self.assertIs(fake_thread.daemon, True)
  337. self.assertTrue(fake_thread.start.called)
  338. class TestCheckErrors(SimpleTestCase):
  339. def test_mutates_error_files(self):
  340. fake_method = mock.MagicMock(side_effect=RuntimeError())
  341. wrapped = autoreload.check_errors(fake_method)
  342. with mock.patch.object(autoreload, '_error_files') as mocked_error_files:
  343. try:
  344. with self.assertRaises(RuntimeError):
  345. wrapped()
  346. finally:
  347. autoreload._exception = None
  348. self.assertEqual(mocked_error_files.append.call_count, 1)
  349. class TestRaiseLastException(SimpleTestCase):
  350. @mock.patch('django.utils.autoreload._exception', None)
  351. def test_no_exception(self):
  352. # Should raise no exception if _exception is None
  353. autoreload.raise_last_exception()
  354. def test_raises_exception(self):
  355. class MyException(Exception):
  356. pass
  357. # Create an exception
  358. try:
  359. raise MyException('Test Message')
  360. except MyException:
  361. exc_info = sys.exc_info()
  362. with mock.patch('django.utils.autoreload._exception', exc_info):
  363. with self.assertRaisesMessage(MyException, 'Test Message'):
  364. autoreload.raise_last_exception()
  365. def test_raises_custom_exception(self):
  366. class MyException(Exception):
  367. def __init__(self, msg, extra_context):
  368. super().__init__(msg)
  369. self.extra_context = extra_context
  370. # Create an exception.
  371. try:
  372. raise MyException('Test Message', 'extra context')
  373. except MyException:
  374. exc_info = sys.exc_info()
  375. with mock.patch('django.utils.autoreload._exception', exc_info):
  376. with self.assertRaisesMessage(MyException, 'Test Message'):
  377. autoreload.raise_last_exception()
  378. def test_raises_exception_with_context(self):
  379. try:
  380. raise Exception(2)
  381. except Exception as e:
  382. try:
  383. raise Exception(1) from e
  384. except Exception:
  385. exc_info = sys.exc_info()
  386. with mock.patch('django.utils.autoreload._exception', exc_info):
  387. with self.assertRaises(Exception) as cm:
  388. autoreload.raise_last_exception()
  389. self.assertEqual(cm.exception.args[0], 1)
  390. self.assertEqual(cm.exception.__cause__.args[0], 2)
  391. class RestartWithReloaderTests(SimpleTestCase):
  392. executable = '/usr/bin/python'
  393. def patch_autoreload(self, argv):
  394. patch_call = mock.patch('django.utils.autoreload.subprocess.run', return_value=CompletedProcess(argv, 0))
  395. patches = [
  396. mock.patch('django.utils.autoreload.sys.argv', argv),
  397. mock.patch('django.utils.autoreload.sys.executable', self.executable),
  398. mock.patch('django.utils.autoreload.sys.warnoptions', ['all']),
  399. ]
  400. for p in patches:
  401. p.start()
  402. self.addCleanup(p.stop)
  403. mock_call = patch_call.start()
  404. self.addCleanup(patch_call.stop)
  405. return mock_call
  406. def test_manage_py(self):
  407. with tempfile.TemporaryDirectory() as temp_dir:
  408. script = Path(temp_dir) / 'manage.py'
  409. script.touch()
  410. argv = [str(script), 'runserver']
  411. mock_call = self.patch_autoreload(argv)
  412. with mock.patch('__main__.__spec__', None):
  413. autoreload.restart_with_reloader()
  414. self.assertEqual(mock_call.call_count, 1)
  415. self.assertEqual(
  416. mock_call.call_args[0][0],
  417. [self.executable, '-Wall'] + argv,
  418. )
  419. def test_python_m_django(self):
  420. main = '/usr/lib/pythonX.Y/site-packages/django/__main__.py'
  421. argv = [main, 'runserver']
  422. mock_call = self.patch_autoreload(argv)
  423. with mock.patch('django.__main__.__file__', main):
  424. with mock.patch.dict(sys.modules, {'__main__': django.__main__}):
  425. autoreload.restart_with_reloader()
  426. self.assertEqual(mock_call.call_count, 1)
  427. self.assertEqual(mock_call.call_args[0][0], [self.executable, '-Wall', '-m', 'django'] + argv[1:])
  428. class ReloaderTests(SimpleTestCase):
  429. RELOADER_CLS = None
  430. def setUp(self):
  431. self._tempdir = tempfile.TemporaryDirectory()
  432. self.tempdir = Path(self._tempdir.name).resolve(strict=True).absolute()
  433. self.existing_file = self.ensure_file(self.tempdir / 'test.py')
  434. self.nonexistent_file = (self.tempdir / 'does_not_exist.py').absolute()
  435. self.reloader = self.RELOADER_CLS()
  436. def tearDown(self):
  437. self._tempdir.cleanup()
  438. self.reloader.stop()
  439. def ensure_file(self, path):
  440. path.parent.mkdir(exist_ok=True, parents=True)
  441. path.touch()
  442. # On Linux and Windows updating the mtime of a file using touch() will set a timestamp
  443. # value that is in the past, as the time value for the last kernel tick is used rather
  444. # than getting the correct absolute time.
  445. # To make testing simpler set the mtime to be the observed time when this function is
  446. # called.
  447. self.set_mtime(path, time.time())
  448. return path.absolute()
  449. def set_mtime(self, fp, value):
  450. os.utime(str(fp), (value, value))
  451. def increment_mtime(self, fp, by=1):
  452. current_time = time.time()
  453. self.set_mtime(fp, current_time + by)
  454. @contextlib.contextmanager
  455. def tick_twice(self):
  456. ticker = self.reloader.tick()
  457. next(ticker)
  458. yield
  459. next(ticker)
  460. class IntegrationTests:
  461. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  462. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  463. def test_glob(self, mocked_modules, notify_mock):
  464. non_py_file = self.ensure_file(self.tempdir / 'non_py_file')
  465. self.reloader.watch_dir(self.tempdir, '*.py')
  466. with self.tick_twice():
  467. self.increment_mtime(non_py_file)
  468. self.increment_mtime(self.existing_file)
  469. self.assertEqual(notify_mock.call_count, 1)
  470. self.assertCountEqual(notify_mock.call_args[0], [self.existing_file])
  471. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  472. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  473. def test_multiple_globs(self, mocked_modules, notify_mock):
  474. self.ensure_file(self.tempdir / 'x.test')
  475. self.reloader.watch_dir(self.tempdir, '*.py')
  476. self.reloader.watch_dir(self.tempdir, '*.test')
  477. with self.tick_twice():
  478. self.increment_mtime(self.existing_file)
  479. self.assertEqual(notify_mock.call_count, 1)
  480. self.assertCountEqual(notify_mock.call_args[0], [self.existing_file])
  481. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  482. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  483. def test_overlapping_globs(self, mocked_modules, notify_mock):
  484. self.reloader.watch_dir(self.tempdir, '*.py')
  485. self.reloader.watch_dir(self.tempdir, '*.p*')
  486. with self.tick_twice():
  487. self.increment_mtime(self.existing_file)
  488. self.assertEqual(notify_mock.call_count, 1)
  489. self.assertCountEqual(notify_mock.call_args[0], [self.existing_file])
  490. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  491. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  492. def test_glob_recursive(self, mocked_modules, notify_mock):
  493. non_py_file = self.ensure_file(self.tempdir / 'dir' / 'non_py_file')
  494. py_file = self.ensure_file(self.tempdir / 'dir' / 'file.py')
  495. self.reloader.watch_dir(self.tempdir, '**/*.py')
  496. with self.tick_twice():
  497. self.increment_mtime(non_py_file)
  498. self.increment_mtime(py_file)
  499. self.assertEqual(notify_mock.call_count, 1)
  500. self.assertCountEqual(notify_mock.call_args[0], [py_file])
  501. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  502. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  503. def test_multiple_recursive_globs(self, mocked_modules, notify_mock):
  504. non_py_file = self.ensure_file(self.tempdir / 'dir' / 'test.txt')
  505. py_file = self.ensure_file(self.tempdir / 'dir' / 'file.py')
  506. self.reloader.watch_dir(self.tempdir, '**/*.txt')
  507. self.reloader.watch_dir(self.tempdir, '**/*.py')
  508. with self.tick_twice():
  509. self.increment_mtime(non_py_file)
  510. self.increment_mtime(py_file)
  511. self.assertEqual(notify_mock.call_count, 2)
  512. self.assertCountEqual(notify_mock.call_args_list, [mock.call(py_file), mock.call(non_py_file)])
  513. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  514. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  515. def test_nested_glob_recursive(self, mocked_modules, notify_mock):
  516. inner_py_file = self.ensure_file(self.tempdir / 'dir' / 'file.py')
  517. self.reloader.watch_dir(self.tempdir, '**/*.py')
  518. self.reloader.watch_dir(inner_py_file.parent, '**/*.py')
  519. with self.tick_twice():
  520. self.increment_mtime(inner_py_file)
  521. self.assertEqual(notify_mock.call_count, 1)
  522. self.assertCountEqual(notify_mock.call_args[0], [inner_py_file])
  523. @mock.patch('django.utils.autoreload.BaseReloader.notify_file_changed')
  524. @mock.patch('django.utils.autoreload.iter_all_python_module_files', return_value=frozenset())
  525. def test_overlapping_glob_recursive(self, mocked_modules, notify_mock):
  526. py_file = self.ensure_file(self.tempdir / 'dir' / 'file.py')
  527. self.reloader.watch_dir(self.tempdir, '**/*.p*')
  528. self.reloader.watch_dir(self.tempdir, '**/*.py*')
  529. with self.tick_twice():
  530. self.increment_mtime(py_file)
  531. self.assertEqual(notify_mock.call_count, 1)
  532. self.assertCountEqual(notify_mock.call_args[0], [py_file])
  533. class BaseReloaderTests(ReloaderTests):
  534. RELOADER_CLS = autoreload.BaseReloader
  535. def test_watch_dir_with_unresolvable_path(self):
  536. path = Path('unresolvable_directory')
  537. with mock.patch.object(Path, 'absolute', side_effect=FileNotFoundError):
  538. self.reloader.watch_dir(path, '**/*.mo')
  539. self.assertEqual(list(self.reloader.directory_globs), [])
  540. def test_watch_with_glob(self):
  541. self.reloader.watch_dir(self.tempdir, '*.py')
  542. watched_files = list(self.reloader.watched_files())
  543. self.assertIn(self.existing_file, watched_files)
  544. def test_watch_files_with_recursive_glob(self):
  545. inner_file = self.ensure_file(self.tempdir / 'test' / 'test.py')
  546. self.reloader.watch_dir(self.tempdir, '**/*.py')
  547. watched_files = list(self.reloader.watched_files())
  548. self.assertIn(self.existing_file, watched_files)
  549. self.assertIn(inner_file, watched_files)
  550. def test_run_loop_catches_stopiteration(self):
  551. def mocked_tick():
  552. yield
  553. with mock.patch.object(self.reloader, 'tick', side_effect=mocked_tick) as tick:
  554. self.reloader.run_loop()
  555. self.assertEqual(tick.call_count, 1)
  556. def test_run_loop_stop_and_return(self):
  557. def mocked_tick(*args):
  558. yield
  559. self.reloader.stop()
  560. return # Raises StopIteration
  561. with mock.patch.object(self.reloader, 'tick', side_effect=mocked_tick) as tick:
  562. self.reloader.run_loop()
  563. self.assertEqual(tick.call_count, 1)
  564. def test_wait_for_apps_ready_checks_for_exception(self):
  565. app_reg = Apps()
  566. app_reg.ready_event.set()
  567. # thread.is_alive() is False if it's not started.
  568. dead_thread = threading.Thread()
  569. self.assertFalse(self.reloader.wait_for_apps_ready(app_reg, dead_thread))
  570. def test_wait_for_apps_ready_without_exception(self):
  571. app_reg = Apps()
  572. app_reg.ready_event.set()
  573. thread = mock.MagicMock()
  574. thread.is_alive.return_value = True
  575. self.assertTrue(self.reloader.wait_for_apps_ready(app_reg, thread))
  576. def skip_unless_watchman_available():
  577. try:
  578. autoreload.WatchmanReloader.check_availability()
  579. except WatchmanUnavailable as e:
  580. return skip('Watchman unavailable: %s' % e)
  581. return lambda func: func
  582. @skip_unless_watchman_available()
  583. class WatchmanReloaderTests(ReloaderTests, IntegrationTests):
  584. RELOADER_CLS = autoreload.WatchmanReloader
  585. def setUp(self):
  586. super().setUp()
  587. # Shorten the timeout to speed up tests.
  588. self.reloader.client_timeout = int(os.environ.get('DJANGO_WATCHMAN_TIMEOUT', 2))
  589. def test_watch_glob_ignores_non_existing_directories_two_levels(self):
  590. with mock.patch.object(self.reloader, '_subscribe') as mocked_subscribe:
  591. self.reloader._watch_glob(self.tempdir / 'does_not_exist' / 'more', ['*'])
  592. self.assertFalse(mocked_subscribe.called)
  593. def test_watch_glob_uses_existing_parent_directories(self):
  594. with mock.patch.object(self.reloader, '_subscribe') as mocked_subscribe:
  595. self.reloader._watch_glob(self.tempdir / 'does_not_exist', ['*'])
  596. self.assertSequenceEqual(
  597. mocked_subscribe.call_args[0],
  598. [
  599. self.tempdir, 'glob-parent-does_not_exist:%s' % self.tempdir,
  600. ['anyof', ['match', 'does_not_exist/*', 'wholename']]
  601. ]
  602. )
  603. def test_watch_glob_multiple_patterns(self):
  604. with mock.patch.object(self.reloader, '_subscribe') as mocked_subscribe:
  605. self.reloader._watch_glob(self.tempdir, ['*', '*.py'])
  606. self.assertSequenceEqual(
  607. mocked_subscribe.call_args[0],
  608. [
  609. self.tempdir, 'glob:%s' % self.tempdir,
  610. ['anyof', ['match', '*', 'wholename'], ['match', '*.py', 'wholename']]
  611. ]
  612. )
  613. def test_watched_roots_contains_files(self):
  614. paths = self.reloader.watched_roots([self.existing_file])
  615. self.assertIn(self.existing_file.parent, paths)
  616. def test_watched_roots_contains_directory_globs(self):
  617. self.reloader.watch_dir(self.tempdir, '*.py')
  618. paths = self.reloader.watched_roots([])
  619. self.assertIn(self.tempdir, paths)
  620. def test_watched_roots_contains_sys_path(self):
  621. with extend_sys_path(str(self.tempdir)):
  622. paths = self.reloader.watched_roots([])
  623. self.assertIn(self.tempdir, paths)
  624. def test_check_server_status(self):
  625. self.assertTrue(self.reloader.check_server_status())
  626. def test_check_server_status_raises_error(self):
  627. with mock.patch.object(self.reloader.client, 'query') as mocked_query:
  628. mocked_query.side_effect = Exception()
  629. with self.assertRaises(autoreload.WatchmanUnavailable):
  630. self.reloader.check_server_status()
  631. @mock.patch('pywatchman.client')
  632. def test_check_availability(self, mocked_client):
  633. mocked_client().capabilityCheck.side_effect = Exception()
  634. with self.assertRaisesMessage(WatchmanUnavailable, 'Cannot connect to the watchman service'):
  635. self.RELOADER_CLS.check_availability()
  636. @mock.patch('pywatchman.client')
  637. def test_check_availability_lower_version(self, mocked_client):
  638. mocked_client().capabilityCheck.return_value = {'version': '4.8.10'}
  639. with self.assertRaisesMessage(WatchmanUnavailable, 'Watchman 4.9 or later is required.'):
  640. self.RELOADER_CLS.check_availability()
  641. def test_pywatchman_not_available(self):
  642. with mock.patch.object(autoreload, 'pywatchman') as mocked:
  643. mocked.__bool__.return_value = False
  644. with self.assertRaisesMessage(WatchmanUnavailable, 'pywatchman not installed.'):
  645. self.RELOADER_CLS.check_availability()
  646. def test_update_watches_raises_exceptions(self):
  647. class TestException(Exception):
  648. pass
  649. with mock.patch.object(self.reloader, '_update_watches') as mocked_watches:
  650. with mock.patch.object(self.reloader, 'check_server_status') as mocked_server_status:
  651. mocked_watches.side_effect = TestException()
  652. mocked_server_status.return_value = True
  653. with self.assertRaises(TestException):
  654. self.reloader.update_watches()
  655. self.assertIsInstance(mocked_server_status.call_args[0][0], TestException)
  656. @mock.patch.dict(os.environ, {'DJANGO_WATCHMAN_TIMEOUT': '10'})
  657. def test_setting_timeout_from_environment_variable(self):
  658. self.assertEqual(self.RELOADER_CLS().client_timeout, 10)
  659. @skipIf(on_macos_with_hfs(), "These tests do not work with HFS+ as a filesystem")
  660. class StatReloaderTests(ReloaderTests, IntegrationTests):
  661. RELOADER_CLS = autoreload.StatReloader
  662. def setUp(self):
  663. super().setUp()
  664. # Shorten the sleep time to speed up tests.
  665. self.reloader.SLEEP_TIME = 0.01
  666. @mock.patch('django.utils.autoreload.StatReloader.notify_file_changed')
  667. def test_tick_does_not_trigger_twice(self, mock_notify_file_changed):
  668. with mock.patch.object(self.reloader, 'watched_files', return_value=[self.existing_file]):
  669. ticker = self.reloader.tick()
  670. next(ticker)
  671. self.increment_mtime(self.existing_file)
  672. next(ticker)
  673. next(ticker)
  674. self.assertEqual(mock_notify_file_changed.call_count, 1)
  675. def test_snapshot_files_ignores_missing_files(self):
  676. with mock.patch.object(self.reloader, 'watched_files', return_value=[self.nonexistent_file]):
  677. self.assertEqual(dict(self.reloader.snapshot_files()), {})
  678. def test_snapshot_files_updates(self):
  679. with mock.patch.object(self.reloader, 'watched_files', return_value=[self.existing_file]):
  680. snapshot1 = dict(self.reloader.snapshot_files())
  681. self.assertIn(self.existing_file, snapshot1)
  682. self.increment_mtime(self.existing_file)
  683. snapshot2 = dict(self.reloader.snapshot_files())
  684. self.assertNotEqual(snapshot1[self.existing_file], snapshot2[self.existing_file])
  685. def test_snapshot_files_with_duplicates(self):
  686. with mock.patch.object(self.reloader, 'watched_files', return_value=[self.existing_file, self.existing_file]):
  687. snapshot = list(self.reloader.snapshot_files())
  688. self.assertEqual(len(snapshot), 1)
  689. self.assertEqual(snapshot[0][0], self.existing_file)