您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符
 
 
 
 
 
 

696 行
22 KiB

  1. # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
  2. # License: MIT. See LICENSE
  3. # imports - standard imports
  4. import gzip
  5. import importlib
  6. import json
  7. import os
  8. import shlex
  9. import shutil
  10. import subprocess
  11. import unittest
  12. from contextlib import contextmanager
  13. from functools import wraps
  14. from glob import glob
  15. from typing import List, Optional
  16. from unittest.case import skipIf
  17. from unittest.mock import patch
  18. # imports - third party imports
  19. import click
  20. from click.testing import CliRunner, Result
  21. from click import Command
  22. # imports - module imports
  23. import frappe
  24. import frappe.commands.site
  25. import frappe.commands.utils
  26. import frappe.recorder
  27. from frappe.installer import add_to_installed_apps, remove_app
  28. from frappe.utils import add_to_date, get_bench_path, get_bench_relative_path, now
  29. from frappe.utils.backups import fetch_latest_backups
  30. _result: Optional[Result] = None
  31. TEST_SITE = "commands-site-O4PN2QKA.test" # added random string tag to avoid collisions
  32. CLI_CONTEXT = frappe._dict(sites=[TEST_SITE])
  33. def clean(value) -> str:
  34. """Strips and converts bytes to str
  35. Args:
  36. value ([type]): [description]
  37. Returns:
  38. [type]: [description]
  39. """
  40. if isinstance(value, bytes):
  41. value = value.decode()
  42. if isinstance(value, str):
  43. value = value.strip()
  44. return value
  45. def missing_in_backup(doctypes: List, file: os.PathLike) -> List:
  46. """Returns list of missing doctypes in the backup.
  47. Args:
  48. doctypes (list): List of DocTypes to be checked
  49. file (str): Path of the database file
  50. Returns:
  51. doctypes(list): doctypes that are missing in backup
  52. """
  53. predicate = (
  54. 'COPY public."tab{}"'
  55. if frappe.conf.db_type == "postgres"
  56. else "CREATE TABLE `tab{}`"
  57. )
  58. with gzip.open(file, "rb") as f:
  59. content = f.read().decode("utf8").lower()
  60. return [doctype for doctype in doctypes
  61. if predicate.format(doctype).lower() not in content]
  62. def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
  63. """Checks if the list of doctypes exist in the database.sql.gz file supplied
  64. Args:
  65. doctypes (list): List of DocTypes to be checked
  66. file (str): Path of the database file
  67. Returns:
  68. bool: True if all tables exist
  69. """
  70. missing_doctypes = missing_in_backup(doctypes, file)
  71. return len(missing_doctypes) == 0
  72. @contextmanager
  73. def maintain_locals():
  74. pre_site = frappe.local.site
  75. pre_flags = frappe.local.flags.copy()
  76. pre_db = frappe.local.db
  77. try:
  78. yield
  79. finally:
  80. post_site = getattr(frappe.local, "site", None)
  81. if not post_site or post_site != pre_site:
  82. frappe.init(site=pre_site)
  83. frappe.local.db = pre_db
  84. frappe.local.flags.update(pre_flags)
  85. def pass_test_context(f):
  86. @wraps(f)
  87. def decorated_function(*args, **kwargs):
  88. return f(CLI_CONTEXT, *args, **kwargs)
  89. return decorated_function
  90. @contextmanager
  91. def cli(cmd: Command, args: Optional[List] = None):
  92. with maintain_locals():
  93. global _result
  94. patch_ctx = patch("frappe.commands.pass_context", pass_test_context)
  95. _module = cmd.callback.__module__
  96. _cmd = cmd.callback.__qualname__
  97. __module = importlib.import_module(_module)
  98. patch_ctx.start()
  99. importlib.reload(__module)
  100. click_cmd = getattr(__module, _cmd)
  101. try:
  102. _result = CliRunner().invoke(click_cmd, args=args)
  103. _result.command = str(cmd)
  104. yield _result
  105. finally:
  106. patch_ctx.stop()
  107. __module = importlib.import_module(_module)
  108. importlib.reload(__module)
  109. importlib.invalidate_caches()
  110. class BaseTestCommands(unittest.TestCase):
  111. @classmethod
  112. def setUpClass(cls) -> None:
  113. cls.setup_test_site()
  114. return super().setUpClass()
  115. @classmethod
  116. def execute(self, command, kwargs=None):
  117. site = {"site": frappe.local.site}
  118. cmd_input = None
  119. if kwargs:
  120. cmd_input = kwargs.get("cmd_input", None)
  121. if cmd_input:
  122. if not isinstance(cmd_input, bytes):
  123. raise Exception(
  124. f"The input should be of type bytes, not {type(cmd_input).__name__}"
  125. )
  126. del kwargs["cmd_input"]
  127. kwargs.update(site)
  128. else:
  129. kwargs = site
  130. self.command = " ".join(command.split()).format(**kwargs)
  131. click.secho(self.command, fg="bright_black")
  132. command = shlex.split(self.command)
  133. self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  134. self.stdout = clean(self._proc.stdout)
  135. self.stderr = clean(self._proc.stderr)
  136. self.returncode = clean(self._proc.returncode)
  137. @classmethod
  138. def setup_test_site(cls):
  139. cmd_config = {
  140. "test_site": TEST_SITE,
  141. "admin_password": frappe.conf.admin_password,
  142. "root_login": frappe.conf.root_login,
  143. "root_password": frappe.conf.root_password,
  144. "db_type": frappe.conf.db_type,
  145. }
  146. if not os.path.exists(
  147. os.path.join(TEST_SITE, "site_config.json")
  148. ):
  149. cls.execute(
  150. "bench new-site {test_site} --admin-password {admin_password} --db-type"
  151. " {db_type}",
  152. cmd_config,
  153. )
  154. def _formatMessage(self, msg, standardMsg):
  155. output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)
  156. if not hasattr(self, "command") and _result:
  157. command = _result.command
  158. stdout = _result.stdout_bytes.decode() if _result.stdout_bytes else None
  159. stderr = _result.stderr_bytes.decode() if _result.stderr_bytes else None
  160. returncode = _result.exit_code
  161. else:
  162. command = self.command
  163. stdout = self.stdout
  164. stderr = self.stderr
  165. returncode = self.returncode
  166. cmd_execution_summary = "\n".join([
  167. "-" * 70,
  168. "Last Command Execution Summary:",
  169. "Command: {}".format(command) if command else "",
  170. "Standard Output: {}".format(stdout) if stdout else "",
  171. "Standard Error: {}".format(stderr) if stderr else "",
  172. "Return Code: {}".format(returncode) if returncode else "",
  173. ]).strip()
  174. return "{}\n\n{}".format(output, cmd_execution_summary)
  175. class TestCommands(BaseTestCommands):
  176. def test_execute(self):
  177. # test 1: execute a command expecting a numeric output
  178. self.execute("bench --site {site} execute frappe.db.get_database_size")
  179. self.assertEqual(self.returncode, 0)
  180. self.assertIsInstance(float(self.stdout), float)
  181. # test 2: execute a command expecting an errored output as local won't exist
  182. self.execute("bench --site {site} execute frappe.local.site")
  183. self.assertEqual(self.returncode, 1)
  184. self.assertIsNotNone(self.stderr)
  185. # test 3: execute a command with kwargs
  186. # Note:
  187. # terminal command has been escaped to avoid .format string replacement
  188. # The returned value has quotes which have been trimmed for the test
  189. self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
  190. self.assertEqual(self.returncode, 0)
  191. self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
  192. @unittest.skip
  193. def test_restore(self):
  194. # step 0: create a site to run the test on
  195. global_config = {
  196. "admin_password": frappe.conf.admin_password,
  197. "root_login": frappe.conf.root_login,
  198. "root_password": frappe.conf.root_password,
  199. "db_type": frappe.conf.db_type,
  200. }
  201. site_data = {"test_site": TEST_SITE, **global_config}
  202. for key, value in global_config.items():
  203. if value:
  204. self.execute(f"bench set-config {key} {value} -g")
  205. # test 1: bench restore from full backup
  206. self.execute("bench --site {test_site} backup --ignore-backup-conf", site_data)
  207. self.execute(
  208. "bench --site {test_site} execute frappe.utils.backups.fetch_latest_backups",
  209. site_data,
  210. )
  211. site_data.update({"database": json.loads(self.stdout)["database"]})
  212. self.execute("bench --site {test_site} restore {database}", site_data)
  213. # test 2: restore from partial backup
  214. self.execute("bench --site {test_site} backup --exclude 'ToDo'", site_data)
  215. site_data.update({"kw": "\"{'partial':True}\""})
  216. self.execute(
  217. "bench --site {test_site} execute"
  218. " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
  219. site_data,
  220. )
  221. site_data.update({"database": json.loads(self.stdout)["database"]})
  222. self.execute("bench --site {test_site} restore {database}", site_data)
  223. self.assertEqual(self.returncode, 1)
  224. def test_partial_restore(self):
  225. _now = now()
  226. for num in range(10):
  227. frappe.get_doc({
  228. "doctype": "ToDo",
  229. "date": add_to_date(_now, days=num),
  230. "description": frappe.mock("paragraph")
  231. }).insert()
  232. frappe.db.commit()
  233. todo_count = frappe.db.count("ToDo")
  234. # check if todos exist, create a partial backup and see if the state is the same after restore
  235. self.assertIsNot(todo_count, 0)
  236. self.execute("bench --site {site} backup --only 'ToDo'")
  237. db_path = fetch_latest_backups(partial=True)["database"]
  238. self.assertTrue("partial" in db_path)
  239. frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabToDo`")
  240. frappe.db.commit()
  241. self.execute("bench --site {site} partial-restore {path}", {"path": db_path})
  242. self.assertEqual(self.returncode, 0)
  243. self.assertEqual(frappe.db.count("ToDo"), todo_count)
  244. def test_recorder(self):
  245. frappe.recorder.stop()
  246. self.execute("bench --site {site} start-recording")
  247. frappe.local.cache = {}
  248. self.assertEqual(frappe.recorder.status(), True)
  249. self.execute("bench --site {site} stop-recording")
  250. frappe.local.cache = {}
  251. self.assertEqual(frappe.recorder.status(), False)
  252. def test_remove_from_installed_apps(self):
  253. app = "test_remove_app"
  254. add_to_installed_apps(app)
  255. # check: confirm that add_to_installed_apps added the app in the default
  256. self.execute("bench --site {site} list-apps")
  257. self.assertIn(app, self.stdout)
  258. # test 1: remove app from installed_apps global default
  259. self.execute("bench --site {site} remove-from-installed-apps {app}", {"app": app})
  260. self.assertEqual(self.returncode, 0)
  261. self.execute("bench --site {site} list-apps")
  262. self.assertNotIn(app, self.stdout)
  263. def test_list_apps(self):
  264. # test 1: sanity check for command
  265. self.execute("bench --site all list-apps")
  266. self.assertIsNotNone(self.returncode)
  267. self.assertIsInstance(self.stdout or self.stderr, str)
  268. # test 2: bare functionality for single site
  269. self.execute("bench --site {site} list-apps")
  270. self.assertEqual(self.returncode, 0)
  271. list_apps = set(
  272. _x.split()[0] for _x in self.stdout.split("\n")
  273. )
  274. doctype = frappe.get_single("Installed Applications").installed_applications
  275. if doctype:
  276. installed_apps = set(x.app_name for x in doctype)
  277. else:
  278. installed_apps = set(frappe.get_installed_apps())
  279. self.assertSetEqual(list_apps, installed_apps)
  280. # test 3: parse json format
  281. self.execute("bench --site {site} list-apps --format json")
  282. self.assertEqual(self.returncode, 0)
  283. self.assertIsInstance(json.loads(self.stdout), dict)
  284. self.execute("bench --site {site} list-apps -f json")
  285. self.assertEqual(self.returncode, 0)
  286. self.assertIsInstance(json.loads(self.stdout), dict)
  287. def test_show_config(self):
  288. # test 1: sanity check for command
  289. self.execute("bench --site all show-config")
  290. self.assertEqual(self.returncode, 0)
  291. # test 2: test keys in table text
  292. self.execute(
  293. "bench --site {site} set-config test_key '{second_order}' --parse",
  294. {"second_order": json.dumps({"test_key": "test_value"})},
  295. )
  296. self.execute("bench --site {site} show-config")
  297. self.assertEqual(self.returncode, 0)
  298. self.assertIn("test_key.test_key", self.stdout.split())
  299. self.assertIn("test_value", self.stdout.split())
  300. # test 3: parse json format
  301. self.execute("bench --site all show-config --format json")
  302. self.assertEqual(self.returncode, 0)
  303. self.assertIsInstance(json.loads(self.stdout), dict)
  304. self.execute("bench --site {site} show-config --format json")
  305. self.assertIsInstance(json.loads(self.stdout), dict)
  306. self.execute("bench --site {site} show-config -f json")
  307. self.assertIsInstance(json.loads(self.stdout), dict)
  308. def test_get_bench_relative_path(self):
  309. bench_path = get_bench_path()
  310. test1_path = os.path.join(bench_path, "test1.txt")
  311. test2_path = os.path.join(bench_path, "sites", "test2.txt")
  312. with open(test1_path, "w+") as test1:
  313. test1.write("asdf")
  314. with open(test2_path, "w+") as test2:
  315. test2.write("asdf")
  316. self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
  317. self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
  318. with self.assertRaises(SystemExit):
  319. get_bench_relative_path("test3.txt")
  320. os.remove(test1_path)
  321. os.remove(test2_path)
  322. def test_frappe_site_env(self):
  323. os.putenv('FRAPPE_SITE', frappe.local.site)
  324. self.execute("bench execute frappe.ping")
  325. self.assertEqual(self.returncode, 0)
  326. self.assertIn("pong", self.stdout)
  327. def test_version(self):
  328. self.execute("bench version")
  329. self.assertEqual(self.returncode, 0)
  330. for output in ["legacy", "plain", "table", "json"]:
  331. self.execute(f"bench version -f {output}")
  332. self.assertEqual(self.returncode, 0)
  333. self.execute("bench version -f invalid")
  334. self.assertEqual(self.returncode, 2)
  335. def test_set_password(self):
  336. from frappe.utils.password import check_password
  337. self.execute("bench --site {site} set-password Administrator test1")
  338. self.assertEqual(self.returncode, 0)
  339. self.assertEqual(check_password('Administrator', 'test1'), 'Administrator')
  340. # to release the lock taken by check_password
  341. frappe.db.commit()
  342. self.execute("bench --site {site} set-admin-password test2")
  343. self.assertEqual(self.returncode, 0)
  344. self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')
  345. def test_make_app(self):
  346. user_input = [
  347. b"Test App", # title
  348. b"This app's description contains 'single quotes' and \"double quotes\".", # description
  349. b"Test Publisher", # publisher
  350. b"example@example.org", # email
  351. b"", # icon
  352. b"", # color
  353. b"MIT" # app_license
  354. ]
  355. app_name = "testapp0"
  356. apps_path = os.path.join(get_bench_path(), "apps")
  357. test_app_path = os.path.join(apps_path, app_name)
  358. self.execute(f"bench make-app {apps_path} {app_name}", {"cmd_input": b'\n'.join(user_input)})
  359. self.assertEqual(self.returncode, 0)
  360. self.assertTrue(
  361. os.path.exists(test_app_path)
  362. )
  363. # cleanup
  364. shutil.rmtree(test_app_path)
  365. @skipIf(
  366. not (
  367. frappe.conf.root_password
  368. and frappe.conf.admin_password
  369. and frappe.conf.db_type == "mariadb"
  370. ),
  371. "DB Root password and Admin password not set in config"
  372. )
  373. def test_bench_drop_site_should_archive_site(self):
  374. # TODO: Make this test postgres compatible
  375. site = TEST_SITE
  376. self.execute(
  377. f"bench new-site {site} --force --verbose "
  378. f"--admin-password {frappe.conf.admin_password} "
  379. f"--mariadb-root-password {frappe.conf.root_password} "
  380. f"--db-type {frappe.conf.db_type or 'mariadb'} "
  381. )
  382. self.assertEqual(self.returncode, 0)
  383. self.execute(f"bench drop-site {site} --force --root-password {frappe.conf.root_password}")
  384. self.assertEqual(self.returncode, 0)
  385. bench_path = get_bench_path()
  386. site_directory = os.path.join(bench_path, f'sites/{site}')
  387. self.assertFalse(os.path.exists(site_directory))
  388. archive_directory = os.path.join(bench_path, f'archived/sites/{site}')
  389. self.assertTrue(os.path.exists(archive_directory))
  390. class TestBackups(BaseTestCommands):
  391. backup_map = {
  392. "includes": {
  393. "includes": [
  394. "ToDo",
  395. "Note",
  396. ]
  397. },
  398. "excludes": {
  399. "excludes": [
  400. "Activity Log",
  401. "Access Log",
  402. "Error Log"
  403. ]
  404. }
  405. }
  406. home = os.path.expanduser("~")
  407. site_backup_path = frappe.utils.get_site_path("private", "backups")
  408. def setUp(self):
  409. self.files_to_trash = []
  410. def tearDown(self):
  411. if self._testMethodName == "test_backup":
  412. for file in self.files_to_trash:
  413. os.remove(file)
  414. try:
  415. os.rmdir(os.path.dirname(file))
  416. except OSError:
  417. pass
  418. def test_backup_no_options(self):
  419. """Take a backup without any options
  420. """
  421. before_backup = fetch_latest_backups(partial=True)
  422. self.execute("bench --site {site} backup")
  423. after_backup = fetch_latest_backups(partial=True)
  424. self.assertEqual(self.returncode, 0)
  425. self.assertIn("successfully completed", self.stdout)
  426. self.assertNotEqual(before_backup["database"], after_backup["database"])
  427. def test_backup_with_files(self):
  428. """Take a backup with files (--with-files)
  429. """
  430. before_backup = fetch_latest_backups()
  431. self.execute("bench --site {site} backup --with-files")
  432. after_backup = fetch_latest_backups()
  433. self.assertEqual(self.returncode, 0)
  434. self.assertIn("successfully completed", self.stdout)
  435. self.assertIn("with files", self.stdout)
  436. self.assertNotEqual(before_backup, after_backup)
  437. self.assertIsNotNone(after_backup["public"])
  438. self.assertIsNotNone(after_backup["private"])
  439. def test_backup_with_custom_path(self):
  440. """Backup to a custom path (--backup-path)
  441. """
  442. backup_path = os.path.join(self.home, "backups")
  443. self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path})
  444. self.assertEqual(self.returncode, 0)
  445. self.assertTrue(os.path.exists(backup_path))
  446. self.assertGreaterEqual(len(os.listdir(backup_path)), 2)
  447. def test_backup_with_different_file_paths(self):
  448. """Backup with different file paths (--backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf)
  449. """
  450. kwargs = {
  451. key: os.path.join(self.home, key, value)
  452. for key, value in {
  453. "db_path": "database.sql.gz",
  454. "files_path": "public.tar",
  455. "private_path": "private.tar",
  456. "conf_path": "config.json",
  457. }.items()
  458. }
  459. self.execute(
  460. """bench
  461. --site {site} backup --with-files
  462. --backup-path-db {db_path}
  463. --backup-path-files {files_path}
  464. --backup-path-private-files {private_path}
  465. --backup-path-conf {conf_path}""",
  466. kwargs,
  467. )
  468. self.assertEqual(self.returncode, 0)
  469. for path in kwargs.values():
  470. self.assertTrue(os.path.exists(path))
  471. def test_backup_compress_files(self):
  472. """Take a compressed backup (--compress)
  473. """
  474. self.execute("bench --site {site} backup --with-files --compress")
  475. self.assertEqual(self.returncode, 0)
  476. compressed_files = glob(f"{self.site_backup_path}/*.tgz")
  477. self.assertGreater(len(compressed_files), 0)
  478. def test_backup_verbose(self):
  479. """Take a verbose backup (--verbose)
  480. """
  481. self.execute("bench --site {site} backup --verbose")
  482. self.assertEqual(self.returncode, 0)
  483. def test_backup_only_specific_doctypes(self):
  484. """Take a backup with (include) backup options set in the site config `frappe.conf.backup.includes`
  485. """
  486. self.execute(
  487. "bench --site {site} set-config backup '{includes}' --parse",
  488. {"includes": json.dumps(self.backup_map["includes"])},
  489. )
  490. self.execute("bench --site {site} backup --verbose")
  491. self.assertEqual(self.returncode, 0)
  492. database = fetch_latest_backups(partial=True)["database"]
  493. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  494. def test_backup_excluding_specific_doctypes(self):
  495. """Take a backup with (exclude) backup options set (`frappe.conf.backup.excludes`, `--exclude`)
  496. """
  497. # test 1: take a backup with frappe.conf.backup.excludes
  498. self.execute(
  499. "bench --site {site} set-config backup '{excludes}' --parse",
  500. {"excludes": json.dumps(self.backup_map["excludes"])},
  501. )
  502. self.execute("bench --site {site} backup --verbose")
  503. self.assertEqual(self.returncode, 0)
  504. database = fetch_latest_backups(partial=True)["database"]
  505. self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
  506. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  507. # test 2: take a backup with --exclude
  508. self.execute(
  509. "bench --site {site} backup --exclude '{exclude}'",
  510. {"exclude": ",".join(self.backup_map["excludes"]["excludes"])},
  511. )
  512. self.assertEqual(self.returncode, 0)
  513. database = fetch_latest_backups(partial=True)["database"]
  514. self.assertFalse(exists_in_backup(self.backup_map["excludes"]["excludes"], database))
  515. def test_selective_backup_priority_resolution(self):
  516. """Take a backup with conflicting backup options set (`frappe.conf.excludes`, `--include`)
  517. """
  518. self.execute(
  519. "bench --site {site} backup --include '{include}'",
  520. {"include": ",".join(self.backup_map["includes"]["includes"])},
  521. )
  522. self.assertEqual(self.returncode, 0)
  523. database = fetch_latest_backups(partial=True)["database"]
  524. self.assertEqual([], missing_in_backup(self.backup_map["includes"]["includes"], database))
  525. def test_dont_backup_conf(self):
  526. """Take a backup ignoring frappe.conf.backup settings (with --ignore-backup-conf option)
  527. """
  528. self.execute("bench --site {site} backup --ignore-backup-conf")
  529. self.assertEqual(self.returncode, 0)
  530. database = fetch_latest_backups()["database"]
  531. self.assertEqual([], missing_in_backup(self.backup_map["excludes"]["excludes"], database))
  532. class TestRemoveApp(unittest.TestCase):
  533. def test_delete_modules(self):
  534. from frappe.installer import (
  535. _delete_doctypes,
  536. _delete_modules,
  537. _get_module_linked_doctype_field_map,
  538. )
  539. test_module = frappe.new_doc("Module Def")
  540. test_module.update({"module_name": "RemoveThis", "app_name": "frappe"})
  541. test_module.save()
  542. module_def_linked_doctype = frappe.get_doc({
  543. "doctype": "DocType",
  544. "name": "Doctype linked with module def",
  545. "module": "RemoveThis",
  546. "custom": 1,
  547. "fields": [{
  548. "label": "Modulen't",
  549. "fieldname": "notmodule",
  550. "fieldtype": "Link",
  551. "options": "Module Def"
  552. }]
  553. }).insert()
  554. doctype_to_link_field_map = _get_module_linked_doctype_field_map()
  555. self.assertIn("Report", doctype_to_link_field_map)
  556. self.assertIn(module_def_linked_doctype.name, doctype_to_link_field_map)
  557. self.assertEqual(doctype_to_link_field_map[module_def_linked_doctype.name], "notmodule")
  558. self.assertNotIn("DocType", doctype_to_link_field_map)
  559. doctypes_to_delete = _delete_modules([test_module.module_name], dry_run=False)
  560. self.assertEqual(len(doctypes_to_delete), 1)
  561. _delete_doctypes(doctypes_to_delete, dry_run=False)
  562. self.assertFalse(frappe.db.exists("Module Def", test_module.module_name))
  563. self.assertFalse(frappe.db.exists("DocType", module_def_linked_doctype.name))
  564. def test_dry_run(self):
  565. """Check if dry run in not destructive."""
  566. # nothing to assert, if this fails rest of the test suite will crumble.
  567. remove_app("frappe", dry_run=True, yes=True, no_backup=True)
  568. class TestSiteMigration(BaseTestCommands):
  569. def test_migrate_cli(self):
  570. with cli(frappe.commands.site.migrate) as result:
  571. self.assertTrue(TEST_SITE in result.stdout)
  572. self.assertEqual(result.exit_code, 0)
  573. self.assertEqual(result.exception, None)
  574. class TestBenchBuild(BaseTestCommands):
  575. def test_build_assets(self):
  576. with cli(frappe.commands.utils.build) as result:
  577. self.assertEqual(result.exit_code, 0)
  578. self.assertEqual(result.exception, None)