Skip to content

Commit ddcaea0

Browse files
os_ops::rmdirs (local, remote) was refactored (#207)
* os_ops::rmdirs (local, remote) was refactored LocalOperations::rmdirs - parameter 'retries' was remaned with 'attempts' - if ignore_errors we raise an error RemoteOperations::rmdirs - parameter 'verbose' was removed - method returns bool - we prevent to delete a file * [TestRemoteOperations] New tests for rmdirs are added. * test_pg_ctl_wait_option (local, remote) is corrected
1 parent 3a1d08b commit ddcaea0

File tree

5 files changed

+255
-50
lines changed

5 files changed

+255
-50
lines changed

testgres/operations/local_ops.py

+31-10
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,8 @@ def makedirs(self, path, remove_existing=False):
174174
except FileExistsError:
175175
pass
176176

177-
def rmdirs(self, path, ignore_errors=True, retries=3, delay=1):
177+
# [2025-02-03] Old name of parameter attempts is "retries".
178+
def rmdirs(self, path, ignore_errors=True, attempts=3, delay=1):
178179
"""
179180
Removes a directory and its contents, retrying on failure.
180181
@@ -183,18 +184,38 @@ def rmdirs(self, path, ignore_errors=True, retries=3, delay=1):
183184
:param retries: Number of attempts to remove the directory.
184185
:param delay: Delay between attempts in seconds.
185186
"""
186-
for attempt in range(retries):
187+
assert type(path) == str # noqa: E721
188+
assert type(ignore_errors) == bool # noqa: E721
189+
assert type(attempts) == int # noqa: E721
190+
assert type(delay) == int or type(delay) == float # noqa: E721
191+
assert attempts > 0
192+
assert delay >= 0
193+
194+
attempt = 0
195+
while True:
196+
assert attempt < attempts
197+
attempt += 1
187198
try:
188-
rmtree(path, ignore_errors=ignore_errors)
189-
if not os.path.exists(path):
190-
return True
199+
rmtree(path)
191200
except FileNotFoundError:
192-
return True
201+
pass
193202
except Exception as e:
194-
logging.error(f"Error: Failed to remove directory {path} on attempt {attempt + 1}: {e}")
195-
time.sleep(delay)
196-
logging.error(f"Error: Failed to remove directory {path} after {retries} attempts.")
197-
return False
203+
if attempt < attempt:
204+
errMsg = "Failed to remove directory {0} on attempt {1} ({2}): {3}".format(
205+
path, attempt, type(e).__name__, e
206+
)
207+
logging.warning(errMsg)
208+
time.sleep(delay)
209+
continue
210+
211+
assert attempt == attempts
212+
if not ignore_errors:
213+
raise
214+
215+
return False
216+
217+
# OK!
218+
return True
198219

199220
def listdir(self, path):
200221
return os.listdir(path)

testgres/operations/remote_ops.py

+34-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
import tempfile
66
import io
7+
import logging
78

89
# we support both pg8000 and psycopg2
910
try:
@@ -222,20 +223,45 @@ def makedirs(self, path, remove_existing=False):
222223
raise Exception("Couldn't create dir {} because of error {}".format(path, error))
223224
return result
224225

225-
def rmdirs(self, path, verbose=False, ignore_errors=True):
226+
def rmdirs(self, path, ignore_errors=True):
226227
"""
227228
Remove a directory in the remote server.
228229
Args:
229230
- path (str): The path to the directory to be removed.
230-
- verbose (bool): If True, return exit status, result, and error.
231231
- ignore_errors (bool): If True, do not raise error if directory does not exist.
232232
"""
233-
cmd = "rm -rf {}".format(path)
234-
exit_status, result, error = self.exec_command(cmd, verbose=True)
235-
if verbose:
236-
return exit_status, result, error
237-
else:
238-
return result
233+
assert type(path) == str # noqa: E721
234+
assert type(ignore_errors) == bool # noqa: E721
235+
236+
# ENOENT = 2 - No such file or directory
237+
# ENOTDIR = 20 - Not a directory
238+
239+
cmd1 = [
240+
"if", "[", "-d", path, "]", ";",
241+
"then", "rm", "-rf", path, ";",
242+
"elif", "[", "-e", path, "]", ";",
243+
"then", "{", "echo", "cannot remove '" + path + "': it is not a directory", ">&2", ";", "exit", "20", ";", "}", ";",
244+
"else", "{", "echo", "directory '" + path + "' does not exist", ">&2", ";", "exit", "2", ";", "}", ";",
245+
"fi"
246+
]
247+
248+
cmd2 = ["sh", "-c", subprocess.list2cmdline(cmd1)]
249+
250+
try:
251+
self.exec_command(cmd2, encoding=Helpers.GetDefaultEncoding())
252+
except ExecUtilException as e:
253+
if e.exit_code == 2: # No such file or directory
254+
return True
255+
256+
if not ignore_errors:
257+
raise
258+
259+
errMsg = "Failed to remove directory {0} ({1}): {2}".format(
260+
path, type(e).__name__, e
261+
)
262+
logging.warning(errMsg)
263+
return False
264+
return True
239265

240266
def listdir(self, path):
241267
"""

tests/test_remote.py

+80-12
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ def test_makedirs_and_rmdirs_success(self):
9999
assert not os.path.exists(path)
100100
assert not self.operations.path_exists(path)
101101

102-
def test_makedirs_and_rmdirs_failure(self):
102+
def test_makedirs_failure(self):
103103
"""
104-
Test makedirs and rmdirs for directory creation and removal failure.
104+
Test makedirs for failure.
105105
"""
106106
# Try to create a directory in a read-only location
107107
path = "/root/test_dir"
@@ -110,16 +110,84 @@ def test_makedirs_and_rmdirs_failure(self):
110110
with pytest.raises(Exception):
111111
self.operations.makedirs(path)
112112

113-
# Test rmdirs
114-
while True:
115-
try:
116-
self.operations.rmdirs(path, verbose=True)
117-
except ExecUtilException as e:
118-
assert e.message == "Utility exited with non-zero code (1). Error: `rm: cannot remove '/root/test_dir': Permission denied`"
119-
assert type(e.error) == bytes # noqa: E721
120-
assert e.error.strip() == b"rm: cannot remove '/root/test_dir': Permission denied"
121-
break
122-
raise Exception("We wait an exception!")
113+
def test_rmdirs(self):
114+
path = self.operations.mkdtemp()
115+
assert os.path.exists(path)
116+
117+
assert self.operations.rmdirs(path, ignore_errors=False) is True
118+
assert not os.path.exists(path)
119+
120+
def test_rmdirs__01_with_subfolder(self):
121+
# folder with subfolder
122+
path = self.operations.mkdtemp()
123+
assert os.path.exists(path)
124+
125+
dir1 = os.path.join(path, "dir1")
126+
assert not os.path.exists(dir1)
127+
128+
self.operations.makedirs(dir1)
129+
assert os.path.exists(dir1)
130+
131+
assert self.operations.rmdirs(path, ignore_errors=False) is True
132+
assert not os.path.exists(path)
133+
assert not os.path.exists(dir1)
134+
135+
def test_rmdirs__02_with_file(self):
136+
# folder with file
137+
path = self.operations.mkdtemp()
138+
assert os.path.exists(path)
139+
140+
file1 = os.path.join(path, "file1.txt")
141+
assert not os.path.exists(file1)
142+
143+
self.operations.touch(file1)
144+
assert os.path.exists(file1)
145+
146+
assert self.operations.rmdirs(path, ignore_errors=False) is True
147+
assert not os.path.exists(path)
148+
assert not os.path.exists(file1)
149+
150+
def test_rmdirs__03_with_subfolder_and_file(self):
151+
# folder with subfolder and file
152+
path = self.operations.mkdtemp()
153+
assert os.path.exists(path)
154+
155+
dir1 = os.path.join(path, "dir1")
156+
assert not os.path.exists(dir1)
157+
158+
self.operations.makedirs(dir1)
159+
assert os.path.exists(dir1)
160+
161+
file1 = os.path.join(dir1, "file1.txt")
162+
assert not os.path.exists(file1)
163+
164+
self.operations.touch(file1)
165+
assert os.path.exists(file1)
166+
167+
assert self.operations.rmdirs(path, ignore_errors=False) is True
168+
assert not os.path.exists(path)
169+
assert not os.path.exists(dir1)
170+
assert not os.path.exists(file1)
171+
172+
def test_rmdirs__try_to_delete_nonexist_path(self):
173+
path = "/root/test_dir"
174+
175+
assert self.operations.rmdirs(path, ignore_errors=False) is True
176+
177+
def test_rmdirs__try_to_delete_file(self):
178+
path = self.operations.mkstemp()
179+
assert os.path.exists(path)
180+
181+
with pytest.raises(ExecUtilException) as x:
182+
self.operations.rmdirs(path, ignore_errors=False)
183+
184+
assert os.path.exists(path)
185+
assert type(x.value) == ExecUtilException # noqa: E721
186+
assert x.value.message == "Utility exited with non-zero code (20). Error: `cannot remove '" + path + "': it is not a directory`"
187+
assert type(x.value.error) == str # noqa: E721
188+
assert x.value.error.strip() == "cannot remove '" + path + "': it is not a directory"
189+
assert type(x.value.exit_code) == int # noqa: E721
190+
assert x.value.exit_code == 20
123191

124192
def test_listdir(self):
125193
"""

tests/test_simple.py

+55-10
Original file line numberDiff line numberDiff line change
@@ -428,16 +428,61 @@ def test_backup_wrong_xlog_method(self):
428428
node.backup(xlog_method='wrong')
429429

430430
def test_pg_ctl_wait_option(self):
431-
with get_new_node() as node:
432-
node.init().start(wait=False)
433-
while True:
434-
try:
435-
node.stop(wait=False)
436-
break
437-
except ExecUtilException:
438-
# it's ok to get this exception here since node
439-
# could be not started yet
440-
pass
431+
C_MAX_ATTEMPTS = 50
432+
433+
node = get_new_node()
434+
assert node.status() == testgres.NodeStatus.Uninitialized
435+
node.init()
436+
assert node.status() == testgres.NodeStatus.Stopped
437+
node.start(wait=False)
438+
nAttempt = 0
439+
while True:
440+
if nAttempt == C_MAX_ATTEMPTS:
441+
raise Exception("Could not stop node.")
442+
443+
nAttempt += 1
444+
445+
if nAttempt > 1:
446+
logging.info("Wait 1 second.")
447+
time.sleep(1)
448+
logging.info("")
449+
450+
logging.info("Try to stop node. Attempt #{0}.".format(nAttempt))
451+
452+
try:
453+
node.stop(wait=False)
454+
break
455+
except ExecUtilException as e:
456+
# it's ok to get this exception here since node
457+
# could be not started yet
458+
logging.info("Node is not stopped. Exception ({0}): {1}".format(type(e).__name__, e))
459+
continue
460+
461+
logging.info("OK. Stop command was executed. Let's wait while our node will stop really.")
462+
nAttempt = 0
463+
while True:
464+
if nAttempt == C_MAX_ATTEMPTS:
465+
raise Exception("Could not stop node.")
466+
467+
nAttempt += 1
468+
if nAttempt > 1:
469+
logging.info("Wait 1 second.")
470+
time.sleep(1)
471+
logging.info("")
472+
473+
logging.info("Attempt #{0}.".format(nAttempt))
474+
s1 = node.status()
475+
476+
if s1 == testgres.NodeStatus.Running:
477+
continue
478+
479+
if s1 == testgres.NodeStatus.Stopped:
480+
break
481+
482+
raise Exception("Unexpected node status: {0}.".format(s1))
483+
484+
logging.info("OK. Node is stopped.")
485+
node.cleanup()
441486

442487
def test_replicate(self):
443488
with get_new_node() as node:

tests/test_simple_remote.py

+55-10
Original file line numberDiff line numberDiff line change
@@ -499,16 +499,61 @@ def test_backup_wrong_xlog_method(self):
499499
node.backup(xlog_method='wrong')
500500

501501
def test_pg_ctl_wait_option(self):
502-
with __class__.helper__get_node() as node:
503-
node.init().start(wait=False)
504-
while True:
505-
try:
506-
node.stop(wait=False)
507-
break
508-
except ExecUtilException:
509-
# it's ok to get this exception here since node
510-
# could be not started yet
511-
pass
502+
C_MAX_ATTEMPTS = 50
503+
504+
node = __class__.helper__get_node()
505+
assert node.status() == testgres.NodeStatus.Uninitialized
506+
node.init()
507+
assert node.status() == testgres.NodeStatus.Stopped
508+
node.start(wait=False)
509+
nAttempt = 0
510+
while True:
511+
if nAttempt == C_MAX_ATTEMPTS:
512+
raise Exception("Could not stop node.")
513+
514+
nAttempt += 1
515+
516+
if nAttempt > 1:
517+
logging.info("Wait 1 second.")
518+
time.sleep(1)
519+
logging.info("")
520+
521+
logging.info("Try to stop node. Attempt #{0}.".format(nAttempt))
522+
523+
try:
524+
node.stop(wait=False)
525+
break
526+
except ExecUtilException as e:
527+
# it's ok to get this exception here since node
528+
# could be not started yet
529+
logging.info("Node is not stopped. Exception ({0}): {1}".format(type(e).__name__, e))
530+
continue
531+
532+
logging.info("OK. Stop command was executed. Let's wait while our node will stop really.")
533+
nAttempt = 0
534+
while True:
535+
if nAttempt == C_MAX_ATTEMPTS:
536+
raise Exception("Could not stop node.")
537+
538+
nAttempt += 1
539+
if nAttempt > 1:
540+
logging.info("Wait 1 second.")
541+
time.sleep(1)
542+
logging.info("")
543+
544+
logging.info("Attempt #{0}.".format(nAttempt))
545+
s1 = node.status()
546+
547+
if s1 == testgres.NodeStatus.Running:
548+
continue
549+
550+
if s1 == testgres.NodeStatus.Stopped:
551+
break
552+
553+
raise Exception("Unexpected node status: {0}.".format(s1))
554+
555+
logging.info("OK. Node is stopped.")
556+
node.cleanup()
512557

513558
def test_replicate(self):
514559
with __class__.helper__get_node() as node:

0 commit comments

Comments
 (0)