unit_test_layout_tool.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import hashlib
  9. import os
  10. import pytest
  11. import shutil
  12. import subprocess
  13. import sys
  14. import tempfile
  15. ROOT_DEV_PATH = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', '..'))
  16. if ROOT_DEV_PATH not in sys.path:
  17. sys.path.append(ROOT_DEV_PATH)
  18. from cmake.Tools import common, layout_tool
  19. def test_copy_asset_files_to_layout_success():
  20. # Mock functions and preserve the originals to restore
  21. old_os_listdir = os.listdir
  22. old_os_path_isdir = os.path.isdir
  23. old_os_path_isfile = os.path.isfile
  24. old_common_filefingerprint = common.file_fingerprint
  25. old_shutil_copy2 = shutil.copy2
  26. try:
  27. # Setup test vectors
  28. # Denied files, should not show up in the result
  29. test_denylist_file = [
  30. 'assetprocessorplatformconfig.setreg'
  31. ]
  32. # System files that are not the same platform, so should skip
  33. test_skip_system_files = [
  34. 'system_badplatform_pc',
  35. 'system_badplatform_badplatform'
  36. ]
  37. # Source 'folders' will be skipped
  38. test_skip_source_folders = [
  39. 'src_folder'
  40. ]
  41. # Destination 'folders' will be skipped
  42. test_skip_dest_is_folder = [
  43. 'fake_dst_folder'
  44. ]
  45. # Skip files that are the same in the destination
  46. test_dest_same_as_src = [
  47. 'dst_same_as_src'
  48. ]
  49. # COPY files that are the same in the destination
  50. test_dest_diff_as_src = [
  51. 'dst_diff_as_src'
  52. ]
  53. # COPY files that are not in the destination
  54. test_src_not_in_dst = [
  55. 'system_goodplatform_pc',
  56. 'good_src_1',
  57. 'good_src_2'
  58. ]
  59. test_expected_copied_files = test_dest_diff_as_src + test_src_not_in_dst
  60. test_game_asset_folder = 'game_cache'
  61. test_layout_target = 'layout_target'
  62. test_platform = 'goodplatform'
  63. def _mock_os_listdir(path):
  64. assert path == test_game_asset_folder
  65. mock_files = test_denylist_file + \
  66. test_skip_system_files + \
  67. test_skip_source_folders + \
  68. test_skip_dest_is_folder + \
  69. test_dest_same_as_src + \
  70. test_dest_diff_as_src + \
  71. test_src_not_in_dst
  72. return mock_files
  73. os.listdir = _mock_os_listdir
  74. def _mock_os_path_isdir(path):
  75. basename = os.path.basename(path)
  76. if basename in test_skip_source_folders:
  77. return True
  78. if basename in test_skip_dest_is_folder:
  79. return True
  80. return False
  81. os.path.isdir = _mock_os_path_isdir
  82. def _mock_os_path_isfile(path):
  83. basename = os.path.basename(path)
  84. if basename in test_dest_same_as_src:
  85. return True
  86. if basename in test_dest_diff_as_src:
  87. return True
  88. return False
  89. os.path.isfile = _mock_os_path_isfile
  90. def _mock_common_file_fingerprint(path):
  91. basename = os.path.basename(path)
  92. dirname = os.path.dirname(path)
  93. if basename in test_dest_same_as_src:
  94. return "SOURCE_FINGERPRINT"
  95. elif basename in test_dest_diff_as_src:
  96. if dirname == test_game_asset_folder:
  97. return "SOURCE_FINGERPRINT"
  98. else:
  99. return "TARGET_FINGERPRINT"
  100. else:
  101. assert False
  102. common.file_fingerprint = _mock_common_file_fingerprint
  103. result_copy_files = []
  104. def _mock_shutil_copy2(src, dst):
  105. assert os.path.basename(src) == os.path.basename(dst)
  106. basename = os.path.basename(dst)
  107. result_copy_files.append(basename)
  108. shutil.copy2 = _mock_shutil_copy2
  109. layout_tool.copy_asset_files_to_layout(project_asset_folder=test_game_asset_folder,
  110. target_platform=test_platform,
  111. layout_target=test_layout_target)
  112. assert len(test_expected_copied_files) == len(result_copy_files)
  113. for expected_copied_file in test_expected_copied_files:
  114. assert expected_copied_file in result_copy_files
  115. finally:
  116. os.listdir = old_os_listdir
  117. os.path.isdir = old_os_path_isdir
  118. os.path.isfile = old_os_path_isfile
  119. common.file_fingerprint = old_common_filefingerprint
  120. shutil.copy2 = old_shutil_copy2
  121. def test_create_link_windows_success():
  122. old_platform = layout_tool.PLATFORM_NAME
  123. old_subprocess_check_call = subprocess.check_call
  124. try:
  125. layout_tool.PLATFORM_NAME = 'Windows'
  126. src = "test_src"
  127. dst = "test_dst"
  128. expected = ['cmd', '/c', 'mklink', '/J', dst, src]
  129. def _mock_subprocess_check_call(args, stdout=None, stderr=None):
  130. assert args == expected
  131. subprocess.check_call = _mock_subprocess_check_call
  132. layout_tool.create_link(src, dst, False)
  133. finally:
  134. layout_tool.PLATFORM_NAME = old_platform
  135. subprocess.check_call = old_subprocess_check_call
  136. def test_create_link_mac_success():
  137. old_platform = layout_tool.PLATFORM_NAME
  138. old_subprocess_check_call = subprocess.check_call
  139. try:
  140. layout_tool.PLATFORM_NAME = 'Darwin'
  141. src = "test_src"
  142. dst = "test_dst"
  143. expected = ['ln', '-s', src, dst]
  144. def _mock_subprocess_check_call(args, stdout=None, stderr=None):
  145. assert args == expected
  146. subprocess.check_call = _mock_subprocess_check_call
  147. layout_tool.create_link(src, dst, False)
  148. finally:
  149. layout_tool.PLATFORM_NAME = old_platform
  150. subprocess.check_call = old_subprocess_check_call
  151. def test_create_link_error():
  152. old_platform = layout_tool.PLATFORM_NAME
  153. old_subprocess_check_call = subprocess.check_call
  154. try:
  155. layout_tool.PLATFORM_NAME = 'Windows'
  156. src = "test_src"
  157. dst = "test_dst"
  158. def _mock_subprocess_check_call(args, stdout=None, stderr=None):
  159. raise subprocess.CalledProcessError(1, "Bad Call")
  160. subprocess.check_call = _mock_subprocess_check_call
  161. layout_tool.create_link(src, dst, False)
  162. except common.LmbrCmdError:
  163. pass
  164. else:
  165. assert False, "subprocess.CalledProcessError exception expected"
  166. finally:
  167. layout_tool.PLATFORM_NAME = old_platform
  168. subprocess.check_call = old_subprocess_check_call
  169. @pytest.mark.parametrize(
  170. "project_path, asset_type, warn_on_missing, expected_result", [
  171. pytest.param('Foo', 'pc', False, 'Foo/Cache/pc'),
  172. pytest.param('Foo', 'pc', True, None),
  173. pytest.param('Foo', 'pc', True, None),
  174. pytest.param('Foo', 'pc', False, common.LmbrCmdError),
  175. pytest.param('Foo', 'pc', False, common.LmbrCmdError),
  176. ]
  177. )
  178. def test_construct_and_validate_cache_game_asset_folder_success(tmpdir, project_path, asset_type, warn_on_missing, expected_result):
  179. if isinstance(expected_result, str):
  180. expected_path_realpath = str(tmpdir.join(expected_result).realpath())
  181. elif expected_result == common.LmbrCmdError:
  182. expected_path_realpath = common.LmbrCmdError
  183. else:
  184. expected_path_realpath = None
  185. try:
  186. result = layout_tool.construct_and_validate_cache_project_asset_folder(project_path=project_path,
  187. asset_type=asset_type,
  188. warn_on_missing_project_cache=warn_on_missing)
  189. assert expected_result != common.LmbrCmdError, "Expecting an error result"
  190. if result == None:
  191. assert warn_on_missing, "Expecting a warn_on_missing==True if None is returned"
  192. elif isinstance(expected_result, str):
  193. assert os.path.normcase(result) == os.path.normcase(expected_path_realpath)
  194. except common.LmbrCmdError:
  195. assert expected_result == common.LmbrCmdError
  196. @pytest.mark.parametrize(
  197. "existing_temp_vfs_folder, existing_gems_link, existing_game_link", [
  198. pytest.param(False, False, False),
  199. pytest.param(True, False, False),
  200. pytest.param(False, True, False),
  201. pytest.param(True, True, False),
  202. pytest.param(False, False, True),
  203. pytest.param(True, False, True),
  204. pytest.param(False, True, True),
  205. pytest.param(True, True, True)
  206. ]
  207. )
  208. def test_sync_layout_vfs_success(tmpdir, existing_temp_vfs_folder, existing_gems_link, existing_game_link):
  209. old_tempfile_gettempdir = tempfile.gettempdir
  210. old_create_link = layout_tool.create_link
  211. old_copy_asset_files_to_layout = layout_tool.copy_asset_files_to_layout
  212. old_rmdir = os.rmdir
  213. old_unlink = os.unlink
  214. try:
  215. # Simple Test Parameters
  216. test_engine_root = str(tmpdir.join('engine-root').realpath())
  217. test_project_path = str(tmpdir.join('Foo').realpath())
  218. test_project_name_lower = 'foo'
  219. test_target_platform = 'bogus'
  220. test_asset_type = 'pc'
  221. # Setup a test dev and game cache folder structure inside the temp folder
  222. # Capture relevant real paths in the temp folder so we can verify our assertions
  223. cache_game_folder = os.path.join(test_project_path, 'Cache')
  224. cache_game_folder_gems = os.path.join(cache_game_folder, test_asset_type, 'gems')
  225. layout_target_root_realpath = str(tmpdir.join('layout').realpath())
  226. layout_target_gems_realpath = os.path.join(layout_target_root_realpath, 'gems')
  227. layout_target_game_realpath = os.path.join(layout_target_root_realpath)
  228. # If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
  229. actual_rmdir_paths = set()
  230. expected_rmdir_paths = set()
  231. # The rmdir will serve as a wrapper to track the paths that are actually deleted
  232. def _mock_os_rmdir(path):
  233. actual_rmdir_paths.add(os.path.normcase(path))
  234. old_rmdir(path)
  235. def _mock_os_unlink(link):
  236. actual_rmdir_paths.add(os.path.normcase(link))
  237. if existing_gems_link:
  238. # Optionally make a dummy folder for the target layout for gems and add it to the expected folder to delete
  239. os.makedirs(layout_target_gems_realpath, exist_ok=False)
  240. expected_rmdir_paths.add(os.path.normcase(layout_target_gems_realpath))
  241. os.rmdir = _mock_os_rmdir
  242. os.unlink = _mock_os_unlink
  243. if existing_game_link:
  244. # Optionally make a dummy folder for the target layout for the game folder and add it to the expected folder to delete
  245. os.makedirs(layout_target_game_realpath, exist_ok=False)
  246. expected_rmdir_paths.add(os.path.normcase(layout_target_game_realpath))
  247. os.rmdir = _mock_os_rmdir
  248. os.unlink = _mock_os_unlink
  249. def _mock_gettempdir():
  250. # mock tempfile.gettempdir() to use tmpdir from pytest
  251. return str(tmpdir.realpath())
  252. tempfile.gettempdir = _mock_gettempdir
  253. # Predict the temp folder name
  254. hasher = hashlib.md5()
  255. hasher.update(test_project_path.encode('UTF-8'))
  256. result = hasher.hexdigest()
  257. tmp_folder_subfolder = 'ly-layout-{}'.format(result)
  258. test_layout_folder = str(tmpdir.join('{}/vfs/foo'.format(tmp_folder_subfolder)).realpath())
  259. test_layout_config_folder = str(tmpdir.join('{}/vfs/foo/config'.format(tmp_folder_subfolder)).realpath())
  260. test_override_pak_folder = ''
  261. if existing_temp_vfs_folder:
  262. # Optionally make a dummy folder for the temp vfs and add the test layout folder and its child config to the expected folders to delete
  263. os.makedirs(test_layout_config_folder, exist_ok=False)
  264. expected_rmdir_paths.add(os.path.normcase(test_layout_folder))
  265. expected_rmdir_paths.add(os.path.normcase(test_layout_config_folder))
  266. os.rmdir = _mock_os_rmdir
  267. mock_layout_tool_create_link_validation = {
  268. os.path.normcase(cache_game_folder_gems): os.path.normcase(layout_target_gems_realpath),
  269. os.path.normcase(test_layout_folder): os.path.normcase(layout_target_game_realpath)
  270. }
  271. def _mock_layout_tool_create_link(src, dst, copy):
  272. check_src = os.path.normcase(src)
  273. check_dst = os.path.normcase(dst)
  274. assert check_src in mock_layout_tool_create_link_validation, "Unexpected create link call to {}->{}".format(src, dst)
  275. assert mock_layout_tool_create_link_validation[check_src] == check_dst, "Assertion on create linked failed: {}->{}".format(src, dst)
  276. layout_tool.create_link = _mock_layout_tool_create_link
  277. def _mock_copy_asset_files_to_layout(project_path, project_asset_folder, target_platform, layout_target):
  278. # Validate the correct call to copy asset files
  279. assert target_platform == target_platform
  280. assert os.path.normcase(layout_target) == os.path.normcase(layout_target_root_realpath)
  281. layout_tool.copy_asset_files_to_layout = _mock_copy_asset_files_to_layout
  282. layout_tool.sync_layout_vfs(target_platform = test_target_platform,
  283. project_path = test_project_path,
  284. asset_type = test_asset_type,
  285. warning_on_missing_assets = False,
  286. layout_target = layout_target_root_realpath,
  287. override_pak_folder = test_override_pak_folder,
  288. copy = False)
  289. # Verify if any the rmdir calls based on the test parameters
  290. assert actual_rmdir_paths == expected_rmdir_paths
  291. finally:
  292. tempfile.gettempdir = old_tempfile_gettempdir
  293. layout_tool.create_link = old_create_link
  294. layout_tool.copy_asset_files_to_layout = old_copy_asset_files_to_layout
  295. os.rmdir = old_rmdir
  296. os.unlink = old_unlink
  297. @pytest.mark.parametrize(
  298. "mode, existing_game_link, existing_gems_link, test_override_pak_folder", [
  299. pytest.param("LOOSE", False, False, None),
  300. pytest.param("LOOSE", False, True, None),
  301. pytest.param("LOOSE", True, False, None),
  302. pytest.param("LOOSE", True, True, None),
  303. pytest.param("PAK", False, None, None),
  304. pytest.param("PAK", True, None, None),
  305. pytest.param("PAK", False, None, 'override_paks'),
  306. pytest.param("PAK", True, None, 'override_paks')
  307. ]
  308. )
  309. def test_sync_layout_non_vfs_success(tmpdir, mode, existing_game_link, existing_gems_link, test_override_pak_folder):
  310. old_rmdir = os.rmdir
  311. old_copy_asset_files_to_layout = layout_tool.copy_asset_files_to_layout
  312. old_remove_link = layout_tool.remove_link
  313. try:
  314. # Simple Test Parameters
  315. engine_root_realpath = str(tmpdir.join('engine-root').realpath())
  316. test_project_path = str(tmpdir.join('Foo').realpath())
  317. test_project_name_lower = 'foo'
  318. test_target_platform = 'bogus'
  319. test_asset_type = 'pc'
  320. cache_game_folder_realpath = os.path.join(test_project_path, 'Cache')
  321. # Make sure a dummy layout folder is created
  322. tmpdir.ensure('layout/dummy.txt')
  323. test_layout_target_realpath = str(tmpdir.join('layout').realpath())
  324. test_layout_target_gems_realpath = os.path.join(test_layout_target_realpath, 'gems')
  325. test_layout_target_game_realpath = os.path.join(test_layout_target_realpath,)
  326. # If we are optionally testing existing links in a layout folder, track the expected and actual rmdirs
  327. actual_rmdir_paths = set()
  328. expected_rmdir_paths = set()
  329. def _mock_remove_link(link):
  330. actual_rmdir_paths.add(os.path.normcase(link))
  331. layout_tool.remove_link = _mock_remove_link
  332. # The rmdir will serve as a wrapper to track the paths that are actually deleted
  333. def _mock_os_rmdir(path):
  334. actual_rmdir_paths.add(os.path.normcase(path))
  335. old_rmdir(path)
  336. if existing_game_link:
  337. # Optionally make a dummy folder for the target layout for the game folder and add it to the expected folder to delete
  338. os.makedirs(test_layout_target_game_realpath, exist_ok=False)
  339. expected_rmdir_paths.add(os.path.normcase(test_layout_target_game_realpath))
  340. os.rmdir = _mock_os_rmdir
  341. mock_layout_tool_create_link_validation = {}
  342. if mode == 'PAK':
  343. # In PAK Mode, the linking rules are slightly different. The 'game folder' link points to inside the pak folder, and there is no 'gems' link
  344. if test_override_pak_folder:
  345. test_game_asset_folder = os.path.join(engine_root_realpath, test_override_pak_folder,
  346. f'{test_project_name_lower}_{test_asset_type}_paks')
  347. cache_game_folder_game_realpath = os.path.join(test_game_asset_folder)
  348. else:
  349. test_game_asset_folder = os.path.join(engine_root_realpath, 'Pak',
  350. f'{test_project_name_lower}_{test_asset_type}_paks')
  351. cache_game_folder_game_realpath = os.path.join(test_game_asset_folder)
  352. mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_game_realpath)] = os.path.normcase(test_layout_target_game_realpath)
  353. elif mode == "LOOSE":
  354. # In LOOSE Mode, both game and gems will be linked
  355. if existing_gems_link:
  356. # Optionally make a dummy folder for the target layout for gems and add it to the expected folder to delete
  357. os.makedirs(test_layout_target_gems_realpath, exist_ok=False)
  358. expected_rmdir_paths.add(os.path.normcase(test_layout_target_gems_realpath))
  359. os.rmdir = _mock_os_rmdir
  360. test_game_asset_folder = os.path.join(cache_game_folder_realpath, test_asset_type)
  361. cache_game_folder_gems_realpath = os.path.join(cache_game_folder_realpath, test_asset_type, 'gems')
  362. cache_game_folder_game_realpath = os.path.join(cache_game_folder_realpath, test_asset_type)
  363. mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_gems_realpath)] = os.path.normcase(test_layout_target_gems_realpath)
  364. mock_layout_tool_create_link_validation[os.path.normcase(cache_game_folder_game_realpath)] = os.path.normcase(test_layout_target_game_realpath)
  365. else:
  366. assert False, "Invalid Mode {}".format(mode)
  367. os.makedirs(test_game_asset_folder, exist_ok=True)
  368. def _mock_copy_asset_files_to_layout(project_path, project_asset_folder, target_platform, layout_target):
  369. assert os.path.normcase(project_asset_folder) == os.path.normcase(test_game_asset_folder)
  370. assert target_platform == test_target_platform
  371. assert layout_target == test_layout_target_realpath
  372. layout_tool.copy_asset_files_to_layout = _mock_copy_asset_files_to_layout
  373. def _mock_layout_tool_create_link(src, dst, copy):
  374. check_src = os.path.normcase(src)
  375. check_dst = os.path.normcase(dst)
  376. assert check_src in mock_layout_tool_create_link_validation, "Unexpected create link call to {}->{}".format(src, dst)
  377. assert mock_layout_tool_create_link_validation[check_src] == check_dst, "Assertion on create linked failed: {}->{}".format(src, dst)
  378. layout_tool.create_link = _mock_layout_tool_create_link
  379. layout_tool.sync_layout_non_vfs(mode = mode,
  380. target_platform = test_target_platform,
  381. project_path = test_project_path,
  382. asset_type = test_asset_type,
  383. warning_on_missing_assets = False,
  384. layout_target = test_layout_target_realpath,
  385. override_pak_folder = test_override_pak_folder,
  386. copy = False)
  387. assert actual_rmdir_paths == expected_rmdir_paths
  388. pass
  389. finally:
  390. os.rmdir = old_rmdir
  391. layout_tool.copy_asset_files_to_layout = old_copy_asset_files_to_layout
  392. layout_tool.remove_link = old_remove_link