Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions armi/tests/test_mpiFeatures.py
Original file line number Diff line number Diff line change
Expand Up @@ -297,50 +297,50 @@ class MpiPathToolsTests(unittest.TestCase):
def test_cleanPathMpi(self):
"""Simple tests of cleanPath(), in the MPI scenario."""
with TemporaryDirectoryChanger():
# TEST 0: File is not safe to delete, due to name pathing
# TEST 0: File is not safe to delete, due to due not being a temp dir or under FAST_PATH
filePath0 = "test0_cleanPathNoMpi"
open(filePath0, "w").write("something")

self.assertTrue(os.path.exists(filePath0))
with self.assertRaises(Exception):
pathTools.cleanPath(filePath0, mpiRank=context.MPI_RANK)
MPI_COMM.barrier()

# TEST 1: Delete a single file
filePath1 = "test1_cleanPathNoMpi_mongoose"
# TEST 1: Delete a single file under FAST_PATH
filePath1 = os.path.join(context.getFastPath(), "test1_cleanPathNoMpi")
open(filePath1, "w").write("something")

self.assertTrue(os.path.exists(filePath1))
pathTools.cleanPath(filePath1, mpiRank=context.MPI_RANK)
MPI_COMM.barrier()
self.assertFalse(os.path.exists(filePath1))

# TEST 2: Delete an empty directory
dir2 = "mongoose"
# TEST 2: Delete an empty directory under FAST_PATH
dir2 = os.path.join(context.getFastPath(), "gimmeonereason")
os.mkdir(dir2)

self.assertTrue(os.path.exists(dir2))
pathTools.cleanPath(dir2, mpiRank=context.MPI_RANK)
MPI_COMM.barrier()
self.assertFalse(os.path.exists(dir2))

# TEST 3: Delete a directory with two files inside
# create directory
dir3 = "mongoose"
# TEST 3: Delete an empty directory with tempDir=True
dir3 = "tostayhere"
os.mkdir(dir3)

# throw in a couple of simple text files
open(os.path.join(dir3, "file1.txt"), "w").write("something1")
open(os.path.join(dir3, "file2.txt"), "w").write("something2")

# delete the directory and test
self.assertTrue(os.path.exists(dir3))
self.assertTrue(os.path.exists(os.path.join(dir3, "file1.txt")))
self.assertTrue(os.path.exists(os.path.join(dir3, "file2.txt")))
pathTools.cleanPath(dir3, mpiRank=context.MPI_RANK)
pathTools.cleanPath(dir3, mpiRank=context.MPI_RANK, tempDir=True)
MPI_COMM.barrier()
self.assertFalse(os.path.exists(dir3))

# TEST 3: Delete a directory with two files inside with tempDir=True
dir4 = "andilldirrightbackaround"
os.mkdir(dir4)
open(os.path.join(dir4, "file1.txt"), "w").write("something1")
open(os.path.join(dir4, "file2.txt"), "w").write("something2")
self.assertTrue(os.path.exists(dir4))
self.assertTrue(os.path.exists(os.path.join(dir4, "file1.txt")))
self.assertTrue(os.path.exists(os.path.join(dir4, "file2.txt")))
pathTools.cleanPath(dir4, mpiRank=context.MPI_RANK, tempDir=True)
MPI_COMM.barrier()
self.assertFalse(os.path.exists(dir4))


class TestContextMpi(unittest.TestCase):
"""Parallel tests for the Context module."""
Expand Down
7 changes: 6 additions & 1 deletion armi/utils/directoryChangers.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,11 @@ def __init__(
outputPath,
)

# If an application sets this environment variable, all root args in all `TempDirChanger` uses are overriden
# with a different root path. This is useful for running unit tests in a read-only environment.
if os.environ.get("TEMP_ROOT_PATH"):
root = os.environ["TEMP_ROOT_PATH"]

# If no root dir is given, the default path comes from context.getFastPath, which
# *might* be relative to the cwd, making it possible to delete unintended files.
# So this check is here to ensure that if we grab a path from context, it is a
Expand Down Expand Up @@ -293,7 +298,7 @@ def __enter__(self):
def __exit__(self, exc_type, exc_value, traceback):
DirectoryChanger.__exit__(self, exc_type, exc_value, traceback)
try:
pathTools.cleanPath(self.destination, context.MPI_RANK)
pathTools.cleanPath(self.destination, mpiRank=context.MPI_RANK, tempDir=True)
except PermissionError:
if os.name == "nt":
runLog.warning(
Expand Down
3 changes: 2 additions & 1 deletion armi/utils/outputCache.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ def deleteCache(cachedFolder):
if "cache" not in str(cachedFolder).lower():
raise RuntimeError("Cache location must contain keyword: `cache`.")

cleanPath(cachedFolder)
# We can consider caches temporary directories, since they are not important containers for simulation results.
cleanPath(cachedFolder, tempDir=True)
Comment on lines +179 to +180
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes me think we could call it something else, like safeToDelete

but of course anyone using cleanPath thinks their path is safe to delete!



def cacheCall(cacheDir, executablePath, inputPaths, outputFileNames, execute=None, tearDown=None):
Expand Down
43 changes: 14 additions & 29 deletions armi/utils/pathTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,6 @@
from armi import context, runLog
from armi.utils import safeCopy

DO_NOT_CLEAN_PATHS = [
"armiruns",
"failedruns",
"mc2run",
"mongoose",
"shufflebranches",
"snapshot",
"tests",
]


def armiAbsPath(*pathParts):
"""Convert a list of path components to an absolute path, without drive letters if possible."""
Expand Down Expand Up @@ -186,19 +176,12 @@ def moduleAndAttributeExist(pathAttr):
return moduleAttributeName in userSpecifiedModule.__dict__


def cleanPath(path, mpiRank=0):
"""Recursively delete a path.

!!! Be careful with this !!! It can delete the entire cluster.

We add copious os.path.exists checks in case an MPI set of things is trying to delete everything
at the same time. Always check filenames for some special flag when calling this, especially
with full permissions on the cluster. You could accidentally delete everyone's work with one
misplaced line! This doesn't ask questions.
def cleanPath(path, mpiRank=0, tempDir=False):
"""Recursively delete a path. This function checks for a few cases we know to be OK to delete: (1) Any
`TemporaryDirectoryChanger` instance and (2) anything under the ARMI `_FAST_PATH`.
Comment on lines +180 to +181
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not what cleanPath originally was meant for. It's worth a discussion.


Safety nets include an allow-list of paths.

This makes use of shutil.rmtree and os.remove
Be careful with editing this! Do not make it a generic can-delete-anything function, because it could in theory
delete anything a user has write permissions on.

Returns
-------
Expand All @@ -209,25 +192,27 @@ def cleanPath(path, mpiRank=0):
if not os.path.exists(path):
return True

for validPath in DO_NOT_CLEAN_PATHS:
if validPath in path.lower():
valid = True
# Any tempDir can be deleted
if tempDir:
valid = True

if pathlib.Path(context.APP_DATA) in pathlib.Path(path).parents:
# If the path slated for deletion is a subdirectory of _FAST_PATH, then cool, delete.
# _FAST_PATH itself gets deleted on program exit.
if pathlib.Path(path).is_relative_to(pathlib.Path(context.getFastPath())):
valid = True

if not valid:
raise Exception("You tried to delete {0}, but it does not seem safe to do so.".format(path))
raise Exception(f"You tried to delete {path}, but it does not seem safe to do so.")

# delete the file/directory from only one process
# Delete the file/directory from only one process
if mpiRank == context.MPI_RANK:
if os.path.exists(path) and os.path.isdir(path):
shutil.rmtree(path)
elif not os.path.isdir(path):
# it's just a file. Delete it.
os.remove(path)

# Potentially, wait for the deletion to finish.
# Deletions are not immediate, so wait for it to finish.
maxLoops = 6
waitTime = 0.5
loopCounter = 0
Expand Down
40 changes: 20 additions & 20 deletions armi/utils/tests/test_pathTools.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,46 +104,46 @@ def test_moduleAndAttributeExist(self):
def test_cleanPathNoMpi(self):
"""Simple tests of cleanPath(), in the no-MPI scenario."""
with TemporaryDirectoryChanger():
# TEST 0: File is not safe to delete, due to name pathing
# TEST 0: File is not safe to delete, due not being a temp dir or under FAST_PATH
filePath0 = "test0_cleanPathNoMpi"
open(filePath0, "w").write("something")

self.assertTrue(os.path.exists(filePath0))
with self.assertRaises(Exception):
pathTools.cleanPath(filePath0, mpiRank=0)

# TEST 1: Delete a single file
filePath1 = "test1_cleanPathNoMpi_mongoose"
# TEST 1: Delete a single file under FAST_PATH
filePath1 = os.path.join(context.getFastPath(), "test1_cleanPathNoMpi")
open(filePath1, "w").write("something")

self.assertTrue(os.path.exists(filePath1))
pathTools.cleanPath(filePath1, mpiRank=0)
self.assertFalse(os.path.exists(filePath1))

# TEST 2: Delete an empty directory
dir2 = "mongoose"
# TEST 2: Delete an empty directory under FAST_PATH
dir2 = os.path.join(context.getFastPath(), "letitgo")
os.mkdir(dir2)

self.assertTrue(os.path.exists(dir2))
pathTools.cleanPath(dir2, mpiRank=0)
self.assertFalse(os.path.exists(dir2))

# TEST 3: Delete a directory with two files inside
# create directory
dir3 = "mongoose"
# TEST 3: Delete an empty directory with tempDir=True
dir3 = "noyoureadirectory"
os.mkdir(dir3)

# throw in a couple of simple text files
open(os.path.join(dir3, "file1.txt"), "w").write("something1")
open(os.path.join(dir3, "file2.txt"), "w").write("something2")

# delete the directory and test
self.assertTrue(os.path.exists(dir3))
self.assertTrue(os.path.exists(os.path.join(dir3, "file1.txt")))
self.assertTrue(os.path.exists(os.path.join(dir3, "file2.txt")))
pathTools.cleanPath(dir3, mpiRank=0)
pathTools.cleanPath(dir3, mpiRank=0, tempDir=True)
self.assertFalse(os.path.exists(dir3))

# TEST 4: Delete a directory with two files inside with tempDir=True
dir4 = "dirplease"
os.mkdir(dir4)
open(os.path.join(dir4, "file1.txt"), "w").write("something1")
open(os.path.join(dir4, "file2.txt"), "w").write("something2")
# delete the directory and test
self.assertTrue(os.path.exists(dir4))
self.assertTrue(os.path.exists(os.path.join(dir4, "file1.txt")))
self.assertTrue(os.path.exists(os.path.join(dir4, "file2.txt")))
pathTools.cleanPath(dir4, mpiRank=0, tempDir=True)
self.assertFalse(os.path.exists(dir4))

def test_isFilePathNewer(self):
with TemporaryDirectoryChanger():
path1 = "test_isFilePathNewer1.txt"
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies = [
"pluggy>=1.2.0", # Central tool behind the ARMI Plugin system
"pyDOE>=0.3.8", # We import a Latin-hypercube algorithm to explore a phase space
"pyevtk>=1.2.0", # Handles binary VTK visualization files
"ruamel.yaml ; python_version >= '3.11.0'", # Our foundational YAML library
"ruamel.yaml<0.19.0; python_version >= '3.11.0'", # Our foundational YAML library
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we got one of those "someone released a new package and now everything broke" situations.

I didn't look into fixes yet, just testing that this is working first

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh oh.

ruamel.yaml is used widely though ARMI, and another downstream materials repo we know.

This would be a really unfortunate restriction> BUT, I guess, if you're only changing the version of ruamel.yaml for old versions of Python, then this isn't a game stopped.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually this is for >= 3.11

So it's pretty ugly. :-(

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was merged into main already, and we will have to address it in the future. I'll make a ticket and resolve this convo about it

"ruamel.yaml.clib ; python_version >= '3.11.0'", # C-based core of ruamel below
"ruamel.yaml.clib<=0.2.7 ; python_version < '3.11.0'", # C-based core of ruamel below
"ruamel.yaml<=0.17.21 ; python_version < '3.11.0'", # Our foundational YAML library
Expand Down
Loading