Skip to content

Commit 5e5128f

Browse files
committed
[_722] adapt for unittest
1 parent bdfbf69 commit 5e5128f

File tree

2 files changed

+36
-20
lines changed

2 files changed

+36
-20
lines changed

irods/test/data_obj_test.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2951,6 +2951,9 @@ def test_replica_truncate__issue_534(self):
29512951
if data_objs.exists(data_path):
29522952
data_objs.unlink(data_path, force=True)
29532953

2954+
def test_reasonable_handling_of_parallel_download_threads_after_termination_signal__issue_722(self):
2955+
from test_parallel_put_sigint_response.py import test
2956+
test(self)
29542957

29552958
if __name__ == "__main__":
29562959
# let the tests find the parent irods lib

irods/test/modules/test_parallel_put_sigint_response.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,32 @@
1-
import signal,tempfile
21

32
import os
43
import re
4+
from signal import SIGINT, SIGTERM
55
import subprocess
66
import sys
7+
import tempfile
78
import time
89

910
import irods
1011
import irods.helpers
11-
import irods.client_configuration as config
1212
from irods.test import modules as test_modules
13-
from irods.test.modules.test_auto_close_of_data_objects__issue_456 import (
14-
auto_close_data_objects,
15-
)
1613

1714
OBJECT_SIZE = 2*1024**3
1815
OBJECT_NAME = 'data_get_issue__722'
1916
LOCAL_TEMPFILE_NAME = 'data_object_for_issue_722.dat'
2017

2118
_clock_resolution = max(.01, *(time.clock_getres(getattr(time,symbol))
2219
for symbol in dir(time) if symbol.startswith('CLOCK_')))
23-
def wait_till_true( function ):
20+
def wait_till_true(function, timeout=None):
21+
start_time = time.clock_gettime_ns(time.CLOCK_BOOTTIME)
2422
while not (truth_value := function()):
23+
if timeout is not None and (time.clock_gettime_ns(time.CLOCK_BOOTTIME)-start_time)*1e-9 > timeout:
24+
break
2525
time.sleep(_clock_resolution)
2626
return truth_value
2727

2828

29-
def test(
30-
# TODO - accept unittest testcase 'test_instance' param , replace assert's with test_instance.assert
31-
):
29+
def test(test_case, sigs = (SIGINT,SIGTERM)):
3230
"""Creates a child process executing a long get() and ensures the process can be
3331
terminated using SIGINT or SIGTERM.
3432
"""
@@ -38,31 +36,44 @@ def test(
3836
# performs a lengthy data object "get" operation (see the main body of the script, below.)
3937
process = subprocess.Popen([sys.executable, program],
4038
stderr=subprocess.PIPE,
41-
stdout=subprocess.PIPE, text = True)
39+
stdout=subprocess.PIPE,
40+
text = True)
4241

42+
# Wait for download process to reach the point of spawning data transfer threads. In Python 3.9+ versions
43+
# of the concurrent.futures module, these are nondaemon threads and will block the exit of the main thread
44+
# unless measures are taken (#722).
4345
localfile = process.stdout.readline().strip()
44-
assert wait_till_true(lambda:os.path.exists(localfile) and os.stat(localfile).st_size > OBJECT_SIZE//2)
46+
test_case.assertTrue(wait_till_true(lambda:os.path.exists(localfile) and os.stat(localfile).st_size > OBJECT_SIZE//2),
47+
"Parallel download from data_objects.get() probably experienced a fatal error before spawning auxiliary data transfer threads."
48+
)
4549

46-
sig = signal.SIGINT
47-
process.send_signal(sig)
48-
class TestFailed(Exception):pass
49-
try:
50-
assert process.wait(timeout = 15) == -sig
51-
assert re.search('KeyboardInterrupt',process.stderr.read())
52-
except subprocess.TimeoutExpired as timeout_exc:
53-
raise TestFailed("Non-daemon thread(s) probably prevented subprocess's main thread from exiting") from timeout_exc
54-
return True
50+
for sig in sigs:
51+
# Interrupt the sub-process with the given signal.
52+
process.send_signal(sig)
53+
# Assert that this signal is what killed the sub-process, rather than a timed out process "wait" or a natural exit
54+
# due to misproper or incomplete handling of the signal.
55+
try:
56+
test_case.assertEqual(process.wait(timeout = 15), -sig)
57+
except subprocess.TimeoutExpired as timeout_exc:
58+
test_case.fail("Sub-process timed out before exit. Non-daemon thread(s) probably prevented subprocess's main thread from exiting")
59+
# Assert that in the case of SIGINT, the process registered a KeyboardInterrupt.
60+
if sig == SIGINT:
61+
test_case.assertTrue(re.search('KeyboardInterrupt',process.stderr.read()))
5562

5663

5764
if __name__ == "__main__":
65+
# These lines are run only if the module is launched as a process.
5866
session = irods.helpers.make_session()
5967
hc = irods.helpers.home_collection(session)
6068
TESTFILE_FILL = b'_'*(1024*1024)
6169
object_path = f'{hc}/{OBJECT_NAME}'
70+
71+
# Create the object to be downloaded.
6272
with session.data_objects.open(object_path,'w') as f:
6373
for y in range(OBJECT_SIZE//len(TESTFILE_FILL)):
6474
f.write(TESTFILE_FILL)
6575
local_path = None
76+
# Establish where (ie absolute path) to place the downloaded file, i.e. the get() target.
6677
try:
6778
with tempfile.NamedTemporaryFile(prefix='local_file_issue_722.dat', delete = True) as t:
6879
local_path = t.name
@@ -71,8 +82,10 @@ class TestFailed(Exception):pass
7182
print(local_path)
7283
sys.stdout.flush()
7384

85+
# "get" the object
7486
session.data_objects.get(object_path, local_path)
7587
finally:
88+
# Clean up, whether or not the download succeeded.
7689
if local_path is not None and os.path.exists(local_path):
7790
os.unlink(local_path)
7891
if session.data_objects.exists(object_path):

0 commit comments

Comments
 (0)