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

Lines changed: 31 additions & 10 deletions
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

Lines changed: 34 additions & 8 deletions
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

Lines changed: 80 additions & 12 deletions
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

Lines changed: 55 additions & 10 deletions
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

Lines changed: 55 additions & 10 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy