diff --git a/armi/utils/__init__.py b/armi/utils/__init__.py index 2c8cfbb1d..be6dbc234 100644 --- a/armi/utils/__init__.py +++ b/armi/utils/__init__.py @@ -806,17 +806,38 @@ def merge(self, *otherDictionaries) -> None: def safeCopy(src: str, dst: str) -> None: """This copy overwrites ``shutil.copy`` and checks that copy operation is truly completed before continuing.""" - waitTime = 0.01 # 10 ms + # Convert files to OS-independence + src = os.path.abspath(src) + dst = os.path.abspath(dst) if os.path.isdir(dst): dst = os.path.join(dst, os.path.basename(src)) srcSize = os.path.getsize(src) - shutil.copyfile(src, dst) - shutil.copymode(src, dst) + if "win" in sys.platform: + cmd = f'copy "{src}" "{dst}"' + elif "linux" in sys.platform: + cmd = f'cp "{src}" "{dst}"' + else: + raise OSError( + "Cannot perform ``safeCopy`` on files because ARMI only supports " + + "Linux and Windows." + ) + os.system(cmd) + waitTime = 0.01 # 10 ms + maxWaitTime = 1800 # 30 min + totalWaitTime = 0 while True: dstSize = os.path.getsize(dst) if srcSize == dstSize: break time.sleep(waitTime) + totalWaitTime += waitTime + if totalWaitTime > maxWaitTime: + runLog.warning( + f"File copy from {dst} to {src} has failed due to exceeding " + + f"a maximum wait time of {maxWaitTime/60} minutes." + ) + break + runLog.extra("Copied {} -> {}".format(src, dst)) diff --git a/armi/utils/tests/test_utils.py b/armi/utils/tests/test_utils.py index c2131c308..7c763cc6d 100644 --- a/armi/utils/tests/test_utils.py +++ b/armi/utils/tests/test_utils.py @@ -14,6 +14,7 @@ """Testing some utility functions.""" from collections import defaultdict +import os import unittest import numpy as np @@ -21,6 +22,7 @@ from armi import utils from armi.reactor.tests.test_reactors import loadTestReactor from armi.settings.caseSettings import Settings +from armi.tests import mockRunLogs from armi.utils import ( directoryChangers, getPowerFractions, @@ -37,6 +39,7 @@ getCumulativeNodeNum, hasBurnup, codeTiming, + safeCopy, ) @@ -166,6 +169,33 @@ def testFunc(): self.assertEqual(getattr(testFunc, "__doc__"), "Test function docstring.") self.assertEqual(getattr(testFunc, "__name__"), "testFunc") + def test_safeCopy(self): + with directoryChangers.TemporaryDirectoryChanger(): + os.mkdir("dir1") + os.mkdir("dir2") + file1 = "dir1/file1.txt" + with open(file1, "w") as f: + f.write("Hello") + file2 = "dir1\\file2.txt" + with open(file2, "w") as f: + f.write("Hello2") + + with mockRunLogs.BufferLog() as mock: + # Test Linuxy file path + self.assertEqual("", mock.getStdout()) + safeCopy(file1, "dir2") + self.assertIn("Copied", mock.getStdout()) + self.assertIn("file1", mock.getStdout()) + self.assertIn("->", mock.getStdout()) + # Clean up for next safeCopy + mock.emptyStdout() + # Test Windowsy file path + self.assertEqual("", mock.getStdout()) + safeCopy(file2, "dir2") + self.assertIn("Copied", mock.getStdout()) + self.assertIn("file2", mock.getStdout()) + self.assertIn("->", mock.getStdout()) + class CyclesSettingsTests(unittest.TestCase): """ diff --git a/doc/release/0.3.rst b/doc/release/0.3.rst index b2ad25ad6..bf2a51e70 100644 --- a/doc/release/0.3.rst +++ b/doc/release/0.3.rst @@ -38,6 +38,7 @@ API Changes Bug Fixes --------- #. Fixed four bugs with "corners up" hex grids. (`PR#1649 `_) +#. Fixed ``safeCopy`` to work on both Windows and Linux with strict permissions (`PR#1691 `_) #. TBD Quality Work