Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 
 
 
 

468 řádky
15 KiB

  1. # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
  2. # imports - standard imports
  3. import gzip
  4. import json
  5. import os
  6. import shlex
  7. import subprocess
  8. import sys
  9. import unittest
  10. import glob
  11. # imports - module imports
  12. import frappe
  13. import frappe.recorder
  14. from frappe.installer import add_to_installed_apps
  15. from frappe.utils import add_to_date, get_bench_relative_path, now
  16. from frappe.utils.backups import fetch_latest_backups
  17. # TODO: check frappe.cli.coloured_output to set coloured output!
  18. def supports_color():
  19. """
  20. Returns True if the running system's terminal supports color, and False
  21. otherwise.
  22. """
  23. plat = sys.platform
  24. supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 'ANSICON' in os.environ)
  25. # isatty is not always implemented, #6223.
  26. is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
  27. return supported_platform and is_a_tty
  28. class color(dict):
  29. nc = "\033[0m"
  30. blue = "\033[94m"
  31. green = "\033[92m"
  32. yellow = "\033[93m"
  33. red = "\033[91m"
  34. silver = "\033[90m"
  35. def __getattr__(self, key):
  36. if supports_color():
  37. ret = self.get(key)
  38. else:
  39. ret = ""
  40. return ret
  41. def clean(value):
  42. """Strips and converts bytes to str
  43. Args:
  44. value ([type]): [description]
  45. Returns:
  46. [type]: [description]
  47. """
  48. if isinstance(value, bytes):
  49. value = value.decode()
  50. if isinstance(value, str):
  51. value = value.strip()
  52. return value
  53. def missing_in_backup(doctypes, file):
  54. """Returns list of missing doctypes in the backup.
  55. Args:
  56. doctypes (list): List of DocTypes to be checked
  57. file (str): Path of the database file
  58. Returns:
  59. doctypes(list): doctypes that are missing in backup
  60. """
  61. predicate = (
  62. 'COPY public."tab{}"'
  63. if frappe.conf.db_type == "postgres"
  64. else "CREATE TABLE `tab{}`"
  65. )
  66. with gzip.open(file, "rb") as f:
  67. content = f.read().decode("utf8").lower()
  68. return [doctype for doctype in doctypes
  69. if predicate.format(doctype).lower() not in content]
  70. def exists_in_backup(doctypes, file):
  71. """Checks if the list of doctypes exist in the database.sql.gz file supplied
  72. Args:
  73. doctypes (list): List of DocTypes to be checked
  74. file (str): Path of the database file
  75. Returns:
  76. bool: True if all tables exist
  77. """
  78. missing_doctypes = missing_in_backup(doctypes, file)
  79. return len(missing_doctypes) == 0
  80. class BaseTestCommands(unittest.TestCase):
  81. def execute(self, command, kwargs=None):
  82. site = {"site": frappe.local.site}
  83. if kwargs:
  84. kwargs.update(site)
  85. else:
  86. kwargs = site
  87. self.command = " ".join(command.split()).format(**kwargs)
  88. print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
  89. command = shlex.split(self.command)
  90. self._proc = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  91. self.stdout = clean(self._proc.stdout)
  92. self.stderr = clean(self._proc.stderr)
  93. self.returncode = clean(self._proc.returncode)
  94. def _formatMessage(self, msg, standardMsg):
  95. output = super(BaseTestCommands, self)._formatMessage(msg, standardMsg)
  96. cmd_execution_summary = "\n".join([
  97. "-" * 70,
  98. "Last Command Execution Summary:",
  99. "Command: {}".format(self.command) if self.command else "",
  100. "Standard Output: {}".format(self.stdout) if self.stdout else "",
  101. "Standard Error: {}".format(self.stderr) if self.stderr else "",
  102. "Return Code: {}".format(self.returncode) if self.returncode else "",
  103. ]).strip()
  104. return "{}\n\n{}".format(output, cmd_execution_summary)
  105. class TestCommands(BaseTestCommands):
  106. def test_execute(self):
  107. # test 1: execute a command expecting a numeric output
  108. self.execute("bench --site {site} execute frappe.db.get_database_size")
  109. self.assertEqual(self.returncode, 0)
  110. self.assertIsInstance(float(self.stdout), float)
  111. # test 2: execute a command expecting an errored output as local won't exist
  112. self.execute("bench --site {site} execute frappe.local.site")
  113. self.assertEqual(self.returncode, 1)
  114. self.assertIsNotNone(self.stderr)
  115. # test 3: execute a command with kwargs
  116. # Note:
  117. # terminal command has been escaped to avoid .format string replacement
  118. # The returned value has quotes which have been trimmed for the test
  119. self.execute("""bench --site {site} execute frappe.bold --kwargs '{{"text": "DocType"}}'""")
  120. self.assertEqual(self.returncode, 0)
  121. self.assertEqual(self.stdout[1:-1], frappe.bold(text="DocType"))
  122. def test_backup(self):
  123. backup = {
  124. "includes": {
  125. "includes": [
  126. "ToDo",
  127. "Note",
  128. ]
  129. },
  130. "excludes": {
  131. "excludes": [
  132. "Activity Log",
  133. "Access Log",
  134. "Error Log"
  135. ]
  136. }
  137. }
  138. home = os.path.expanduser("~")
  139. site_backup_path = frappe.utils.get_site_path("private", "backups")
  140. # test 1: take a backup
  141. before_backup = fetch_latest_backups()
  142. self.execute("bench --site {site} backup")
  143. after_backup = fetch_latest_backups()
  144. self.assertEqual(self.returncode, 0)
  145. self.assertIn("successfully completed", self.stdout)
  146. self.assertNotEqual(before_backup["database"], after_backup["database"])
  147. # test 2: take a backup with --with-files
  148. before_backup = after_backup.copy()
  149. self.execute("bench --site {site} backup --with-files")
  150. after_backup = fetch_latest_backups()
  151. self.assertEqual(self.returncode, 0)
  152. self.assertIn("successfully completed", self.stdout)
  153. self.assertIn("with files", self.stdout)
  154. self.assertNotEqual(before_backup, after_backup)
  155. self.assertIsNotNone(after_backup["public"])
  156. self.assertIsNotNone(after_backup["private"])
  157. # test 3: take a backup with --backup-path
  158. backup_path = os.path.join(home, "backups")
  159. self.execute("bench --site {site} backup --backup-path {backup_path}", {"backup_path": backup_path})
  160. self.assertEqual(self.returncode, 0)
  161. self.assertTrue(os.path.exists(backup_path))
  162. self.assertGreaterEqual(len(os.listdir(backup_path)), 2)
  163. # test 4: take a backup with --backup-path-db, --backup-path-files, --backup-path-private-files, --backup-path-conf
  164. kwargs = {
  165. key: os.path.join(home, key, value)
  166. for key, value in {
  167. "db_path": "database.sql.gz",
  168. "files_path": "public.tar",
  169. "private_path": "private.tar",
  170. "conf_path": "config.json",
  171. }.items()
  172. }
  173. self.execute(
  174. """bench
  175. --site {site} backup --with-files
  176. --backup-path-db {db_path}
  177. --backup-path-files {files_path}
  178. --backup-path-private-files {private_path}
  179. --backup-path-conf {conf_path}""",
  180. kwargs,
  181. )
  182. self.assertEqual(self.returncode, 0)
  183. for path in kwargs.values():
  184. self.assertTrue(os.path.exists(path))
  185. # test 5: take a backup with --compress
  186. self.execute("bench --site {site} backup --with-files --compress")
  187. self.assertEqual(self.returncode, 0)
  188. compressed_files = glob.glob(site_backup_path + "/*.tgz")
  189. self.assertGreater(len(compressed_files), 0)
  190. # test 6: take a backup with --verbose
  191. self.execute("bench --site {site} backup --verbose")
  192. self.assertEqual(self.returncode, 0)
  193. # test 7: take a backup with frappe.conf.backup.includes
  194. self.execute(
  195. "bench --site {site} set-config backup '{includes}' --parse",
  196. {"includes": json.dumps(backup["includes"])},
  197. )
  198. self.execute("bench --site {site} backup --verbose")
  199. self.assertEqual(self.returncode, 0)
  200. database = fetch_latest_backups(partial=True)["database"]
  201. self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
  202. # test 8: take a backup with frappe.conf.backup.excludes
  203. self.execute(
  204. "bench --site {site} set-config backup '{excludes}' --parse",
  205. {"excludes": json.dumps(backup["excludes"])},
  206. )
  207. self.execute("bench --site {site} backup --verbose")
  208. self.assertEqual(self.returncode, 0)
  209. database = fetch_latest_backups(partial=True)["database"]
  210. self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
  211. self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
  212. # test 9: take a backup with --include (with frappe.conf.excludes still set)
  213. self.execute(
  214. "bench --site {site} backup --include '{include}'",
  215. {"include": ",".join(backup["includes"]["includes"])},
  216. )
  217. self.assertEqual(self.returncode, 0)
  218. database = fetch_latest_backups(partial=True)["database"]
  219. self.assertEqual([], missing_in_backup(backup["includes"]["includes"], database))
  220. # test 10: take a backup with --exclude
  221. self.execute(
  222. "bench --site {site} backup --exclude '{exclude}'",
  223. {"exclude": ",".join(backup["excludes"]["excludes"])},
  224. )
  225. self.assertEqual(self.returncode, 0)
  226. database = fetch_latest_backups(partial=True)["database"]
  227. self.assertFalse(exists_in_backup(backup["excludes"]["excludes"], database))
  228. # test 11: take a backup with --ignore-backup-conf
  229. self.execute("bench --site {site} backup --ignore-backup-conf")
  230. self.assertEqual(self.returncode, 0)
  231. database = fetch_latest_backups()["database"]
  232. self.assertEqual([], missing_in_backup(backup["excludes"]["excludes"], database))
  233. def test_restore(self):
  234. # step 0: create a site to run the test on
  235. global_config = {
  236. "admin_password": frappe.conf.admin_password,
  237. "root_login": frappe.conf.root_login,
  238. "root_password": frappe.conf.root_password,
  239. "db_type": frappe.conf.db_type,
  240. }
  241. site_data = {"another_site": f"{frappe.local.site}-restore.test", **global_config}
  242. for key, value in global_config.items():
  243. if value:
  244. self.execute(f"bench set-config {key} {value} -g")
  245. self.execute(
  246. "bench new-site {another_site} --admin-password {admin_password} --db-type"
  247. " {db_type}",
  248. site_data,
  249. )
  250. # test 1: bench restore from full backup
  251. self.execute("bench --site {another_site} backup --ignore-backup-conf", site_data)
  252. self.execute(
  253. "bench --site {another_site} execute frappe.utils.backups.fetch_latest_backups",
  254. site_data,
  255. )
  256. site_data.update({"database": json.loads(self.stdout)["database"]})
  257. self.execute("bench --site {another_site} restore {database}", site_data)
  258. # test 2: restore from partial backup
  259. self.execute("bench --site {another_site} backup --exclude 'ToDo'", site_data)
  260. site_data.update({"kw": "\"{'partial':True}\""})
  261. self.execute(
  262. "bench --site {another_site} execute"
  263. " frappe.utils.backups.fetch_latest_backups --kwargs {kw}",
  264. site_data,
  265. )
  266. site_data.update({"database": json.loads(self.stdout)["database"]})
  267. self.execute("bench --site {another_site} restore {database}", site_data)
  268. self.assertEqual(self.returncode, 1)
  269. def test_partial_restore(self):
  270. _now = now()
  271. for num in range(10):
  272. frappe.get_doc({
  273. "doctype": "ToDo",
  274. "date": add_to_date(_now, days=num),
  275. "description": frappe.mock("paragraph")
  276. }).insert()
  277. frappe.db.commit()
  278. todo_count = frappe.db.count("ToDo")
  279. # check if todos exist, create a partial backup and see if the state is the same after restore
  280. self.assertIsNot(todo_count, 0)
  281. self.execute("bench --site {site} backup --only 'ToDo'")
  282. db_path = fetch_latest_backups(partial=True)["database"]
  283. self.assertTrue("partial" in db_path)
  284. frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabToDo`")
  285. frappe.db.commit()
  286. self.execute("bench --site {site} partial-restore {path}", {"path": db_path})
  287. self.assertEqual(self.returncode, 0)
  288. self.assertEqual(frappe.db.count("ToDo"), todo_count)
  289. def test_recorder(self):
  290. frappe.recorder.stop()
  291. self.execute("bench --site {site} start-recording")
  292. frappe.local.cache = {}
  293. self.assertEqual(frappe.recorder.status(), True)
  294. self.execute("bench --site {site} stop-recording")
  295. frappe.local.cache = {}
  296. self.assertEqual(frappe.recorder.status(), False)
  297. def test_remove_from_installed_apps(self):
  298. app = "test_remove_app"
  299. add_to_installed_apps(app)
  300. # check: confirm that add_to_installed_apps added the app in the default
  301. self.execute("bench --site {site} list-apps")
  302. self.assertIn(app, self.stdout)
  303. # test 1: remove app from installed_apps global default
  304. self.execute("bench --site {site} remove-from-installed-apps {app}", {"app": app})
  305. self.assertEqual(self.returncode, 0)
  306. self.execute("bench --site {site} list-apps")
  307. self.assertNotIn(app, self.stdout)
  308. def test_list_apps(self):
  309. # test 1: sanity check for command
  310. self.execute("bench --site all list-apps")
  311. self.assertEqual(self.returncode, 0)
  312. # test 2: bare functionality for single site
  313. self.execute("bench --site {site} list-apps")
  314. self.assertEqual(self.returncode, 0)
  315. list_apps = set(
  316. _x.split()[0] for _x in self.stdout.split("\n")
  317. )
  318. doctype = frappe.get_single("Installed Applications").installed_applications
  319. if doctype:
  320. installed_apps = set(x.app_name for x in doctype)
  321. else:
  322. installed_apps = set(frappe.get_installed_apps())
  323. self.assertSetEqual(list_apps, installed_apps)
  324. # test 3: parse json format
  325. self.execute("bench --site all list-apps --format json")
  326. self.assertEqual(self.returncode, 0)
  327. self.assertIsInstance(json.loads(self.stdout), dict)
  328. self.execute("bench --site {site} list-apps --format json")
  329. self.assertIsInstance(json.loads(self.stdout), dict)
  330. self.execute("bench --site {site} list-apps -f json")
  331. self.assertIsInstance(json.loads(self.stdout), dict)
  332. def test_show_config(self):
  333. # test 1: sanity check for command
  334. self.execute("bench --site all show-config")
  335. self.assertEqual(self.returncode, 0)
  336. # test 2: test keys in table text
  337. self.execute(
  338. "bench --site {site} set-config test_key '{second_order}' --parse",
  339. {"second_order": json.dumps({"test_key": "test_value"})},
  340. )
  341. self.execute("bench --site {site} show-config")
  342. self.assertEqual(self.returncode, 0)
  343. self.assertIn("test_key.test_key", self.stdout.split())
  344. self.assertIn("test_value", self.stdout.split())
  345. # test 3: parse json format
  346. self.execute("bench --site all show-config --format json")
  347. self.assertEqual(self.returncode, 0)
  348. self.assertIsInstance(json.loads(self.stdout), dict)
  349. self.execute("bench --site {site} show-config --format json")
  350. self.assertIsInstance(json.loads(self.stdout), dict)
  351. self.execute("bench --site {site} show-config -f json")
  352. self.assertIsInstance(json.loads(self.stdout), dict)
  353. def test_get_bench_relative_path(self):
  354. bench_path = frappe.utils.get_bench_path()
  355. test1_path = os.path.join(bench_path, "test1.txt")
  356. test2_path = os.path.join(bench_path, "sites", "test2.txt")
  357. with open(test1_path, "w+") as test1:
  358. test1.write("asdf")
  359. with open(test2_path, "w+") as test2:
  360. test2.write("asdf")
  361. self.assertTrue("test1.txt" in get_bench_relative_path("test1.txt"))
  362. self.assertTrue("sites/test2.txt" in get_bench_relative_path("test2.txt"))
  363. with self.assertRaises(SystemExit):
  364. get_bench_relative_path("test3.txt")
  365. os.remove(test1_path)
  366. os.remove(test2_path)
  367. def test_frappe_site_env(self):
  368. os.putenv('FRAPPE_SITE', frappe.local.site)
  369. self.execute("bench execute frappe.ping")
  370. self.assertEqual(self.returncode, 0)
  371. self.assertIn("pong", self.stdout)
  372. def test_version(self):
  373. self.execute("bench version")
  374. self.assertEqual(self.returncode, 0)
  375. for output in ["legacy", "plain", "table", "json"]:
  376. self.execute(f"bench version -f {output}")
  377. self.assertEqual(self.returncode, 0)
  378. self.execute("bench version -f invalid")
  379. self.assertEqual(self.returncode, 2)
  380. def test_set_password(self):
  381. from frappe.utils.password import check_password
  382. self.execute("bench --site {site} set-password Administrator test1")
  383. self.assertEqual(self.returncode, 0)
  384. self.assertEqual(check_password('Administrator', 'test1'), 'Administrator')
  385. # to release the lock taken by check_password
  386. frappe.db.commit()
  387. self.execute("bench --site {site} set-admin-password test2")
  388. self.assertEqual(self.returncode, 0)
  389. self.assertEqual(check_password('Administrator', 'test2'), 'Administrator')