Repository navigation
Expand file tree
/
Copy pathtest_thread_utils.py
More file actions
1987 lines (1547 loc) · 67.1 KB
/
Copy pathtest_thread_utils.py
File metadata and controls
1987 lines (1547 loc) · 67.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# ================= QThreadPoolExecutor 测试 =================
import concurrent.futures
from qthreadwithreturn import QThreadPoolExecutor
import sys
import time
import threading
from PySide6.QtCore import QThread
import pytest
from PySide6.QtWidgets import QApplication
# 导入我们要测试的类
from qthreadwithreturn import QThreadWithReturn
# 测试隔离和资源清理装饰器
def cleanup_threads_and_pools(func):
"""测试装饰器,完全禁用清理避免系统崩溃"""
import functools
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 直接执行测试,不进行任何清理操作
# 清理操作可能导致Qt信号递归和栈溢出
return func(*args, **kwargs)
return wrapper
class TestQThreadPoolExecutor:
def test_submit_and_result(self, qapp, simple_function):
"""测试 submit 提交任务和获取结果"""
with QThreadPoolExecutor(max_workers=2) as pool:
future = pool.submit(simple_function, 2, y=3)
assert future.result() == 5
assert future.done()
assert not future.running()
def test_submit_exception(self, qapp, error_function):
"""测试 submit 提交抛出异常的任务"""
with QThreadPoolExecutor(max_workers=1) as pool:
future = pool.submit(error_function)
with pytest.raises(ValueError, match="Test error"):
future.result()
assert future.done()
assert not future.running()
def test_shutdown_wait_and_cancel(self, qapp, slow_function):
"""测试 shutdown 的 wait/cancel_futures/force_stop"""
with QThreadPoolExecutor(max_workers=1) as pool:
fut1 = pool.submit(slow_function, duration=0.2)
fut2 = pool.submit(slow_function, duration=0.5)
pool.shutdown(wait=False, cancel_futures=True)
# fut2 应该被取消
assert fut2.cancelled() or fut2.done()
# fut1 可能已在运行,等待其完成
try:
fut1.result(timeout_ms=1000)
except Exception:
pass
def test_shutdown_force_stop(self, qapp, slow_function):
"""测试 shutdown 的 force_stop 参数"""
with QThreadPoolExecutor(max_workers=1) as pool:
fut = pool.submit(slow_function, duration=1)
pool.shutdown(wait=True, force_stop=True)
# fut 可能被强制终止
assert fut.cancelled() or fut.done()
def test_with_statement(self, qapp, simple_function):
"""测试 with 语句支持"""
with QThreadPoolExecutor(max_workers=1) as pool:
fut = pool.submit(simple_function, 1, y=2)
assert fut.result() == 3
def test_thread_name_prefix_and_initializer(self, qapp, simple_function):
"""测试 thread_name_prefix 和 initializer/initargs"""
names = []
from PySide6.QtCore import QThread
def init():
names.append(QThread.currentThread().objectName())
with QThreadPoolExecutor(
max_workers=1, thread_name_prefix="TestPrefix", initializer=init
) as pool:
fut = pool.submit(simple_function, 1, y=2)
fut.result()
assert any("TestPrefix" in n for n in names)
def test_as_completed(self, qapp, simple_function, slow_function):
"""测试 as_completed 方法"""
with QThreadPoolExecutor(max_workers=2) as pool:
fut1 = pool.submit(slow_function, duration=0.2)
fut2 = pool.submit(simple_function, 1, y=2)
fut3 = pool.submit(simple_function, 2, y=3)
completed = []
for fut in QThreadPoolExecutor.as_completed([fut1, fut2, fut3], timeout_ms=2000):
if fut.done():
completed.append(
fut.result()
if not fut.cancelled() and fut.exception() is None
else None
)
assert set(completed) >= {3, 5, "completed"}
@cleanup_threads_and_pools
def test_submit_after_shutdown_raises(self, qapp, simple_function):
"""shutdown 后 submit 抛异常"""
pool = QThreadPoolExecutor(max_workers=1)
try:
pool.shutdown()
with pytest.raises(RuntimeError):
pool.submit(simple_function, 1, y=2)
finally:
# 确保清理
if not pool._shutdown:
pool.shutdown()
@cleanup_threads_and_pools
def test_multiple_shutdown_calls(self, qapp, simple_function):
"""多次 shutdown 不抛异常"""
pool = QThreadPoolExecutor(max_workers=1)
try:
pool.submit(simple_function, 1, y=2)
pool.shutdown()
pool.shutdown()
finally:
# 确保清理
if not pool._shutdown:
pool.shutdown()
@cleanup_threads_and_pools
def test_force_stop_cancelled_futures(self, qapp, slow_function):
"""force_stop=True 时取消所有未完成任务"""
pool = QThreadPoolExecutor(max_workers=1)
try:
fut1 = pool.submit(slow_function, duration=1)
fut2 = pool.submit(slow_function, duration=1)
pool.shutdown(force_stop=True, cancel_futures=True)
assert fut1.cancelled() or fut1.done()
assert fut2.cancelled() or fut2.done()
finally:
# 确保清理
if not pool._shutdown:
pool.shutdown(force_stop=True)
def test_max_workers_limit(self, qapp):
"""测试线程池最大并发数限制,确保不会超出max_workers"""
from PySide6.QtCore import QMutex, QWaitCondition
active_count = 0
max_active = 0
mutex = QMutex()
cond = QWaitCondition()
results = []
def work(x):
nonlocal active_count, max_active
mutex.lock()
active_count += 1
if active_count > max_active:
max_active = active_count
mutex.unlock()
time.sleep(0.2)
mutex.lock()
active_count -= 1
mutex.unlock()
return x * 2
with QThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(work, i) for i in range(6)]
out = [f.result() for f in futures]
assert max_active == 2, f"最大并发数应为2,实际为{max_active}"
assert out == [0, 2, 4, 6, 8, 10]
def test_queueing_and_order(self, qapp):
"""测试任务排队顺序和结果顺序"""
order = []
def work(x):
time.sleep(0.05 * (5 - x))
order.append(x)
return x
with QThreadPoolExecutor(max_workers=2) as pool:
futures = [pool.submit(work, i) for i in range(6)]
results = [f.result() for f in futures]
# 结果顺序应与提交顺序一致
assert results == list(range(6))
# 实际执行顺序不一定,但所有任务都应执行
assert set(order) == set(range(6))
def test_shutdown_cancel_and_force_stop(self, qapp):
"""测试shutdown各种参数组合的行为"""
def slow():
time.sleep(0.5)
return 1
with QThreadPoolExecutor(max_workers=1) as pool:
fut1 = pool.submit(slow)
fut2 = pool.submit(slow)
pool.shutdown(wait=False, cancel_futures=True, force_stop=True)
assert fut2.cancelled() or fut2.done()
# fut1可能已在运行,等待其完成
try:
fut1.result(timeout_ms=2000)
except Exception:
pass
def test_thread_safety_under_contention(self, qapp):
"""高并发下线程池线程安全性测试"""
counter = 0
lock = threading.Lock()
def work():
nonlocal counter
for _ in range(100):
with lock:
counter += 1
with QThreadPoolExecutor(max_workers=4) as pool:
futures = [pool.submit(work) for _ in range(8)]
for f in futures:
f.result()
assert counter == 8 * 100
def test_initializer_and_initargs(self, qapp):
"""测试initializer和initargs参数"""
names = []
def init(x, y):
import sys
obj_name = QThread.currentThread().objectName()
print(
f"[test_initializer_and_initargs.init] objectName: {obj_name}",
file=sys.stderr,
)
names.append((obj_name, x, y))
def dummy():
return 1
with QThreadPoolExecutor(
max_workers=2,
thread_name_prefix="InitTest",
initializer=init,
initargs=(1, 2),
) as pool:
futs = [pool.submit(dummy) for _ in range(2)]
for f in futs:
f.result()
assert all(n[1:] == (1, 2) for n in names)
# 兼容主线程预调用,至少有2个线程名以InitTest开头
assert sum("InitTest" in n[0] for n in names) >= 2
@cleanup_threads_and_pools
def test_submit_after_shutdown_and_multiple_shutdown(self, qapp, simple_function):
"""shutdown后submit抛异常,多次shutdown不抛异常"""
pool = QThreadPoolExecutor(max_workers=1)
try:
pool.submit(simple_function, 1, y=2)
pool.shutdown()
with pytest.raises(RuntimeError):
pool.submit(simple_function, 1, y=2)
pool.shutdown()
pool.shutdown()
finally:
# 确保清理
if not pool._shutdown:
pool.shutdown()
def test_pool_with_no_param_callback(self, qapp):
"""测试线程池支持无参数回调"""
result_container = {"called": False, "count": 0}
def no_param_callback():
"""无参数回调函数"""
result_container["called"] = True
result_container["count"] += 1
def task():
time.sleep(0.1)
return "pool_result"
with QThreadPoolExecutor(max_workers=2) as pool:
future = pool.submit(task)
future.add_done_callback(no_param_callback)
result = future.result()
assert result == "pool_result"
# 处理Qt事件以确保回调被执行
wait_with_events(200)
assert result_container["called"], "无参数回调应该被调用"
assert result_container["count"] == 1, "回调应该只被调用一次"
def test_pool_with_multiple_return_values(self, qapp):
"""测试线程池支持多返回值解包"""
result_container = {"values": None}
def multi_param_callback(a, b, c):
"""多参数回调函数"""
result_container["values"] = (a, b, c)
def task_with_multiple_returns():
time.sleep(0.1)
return (10, 20, 30)
with QThreadPoolExecutor(max_workers=2) as pool:
future = pool.submit(task_with_multiple_returns)
future.add_done_callback(multi_param_callback)
result = future.result()
assert result == (10, 20, 30)
# 处理Qt事件以确保回调被执行
wait_with_events(200)
assert result_container["values"] == (10, 20, 30), (
"多参数回调应该接收到解包的值"
)
def test_pool_class_method_callbacks(self, qapp):
"""测试线程池支持类方法作为回调"""
class CallbackHandler:
def __init__(self):
self.results = []
self.no_param_count = 0
def handle_result(self, value):
"""带参数的类方法回调"""
self.results.append(value)
def handle_no_param(self):
"""无参数的类方法回调(只有self)"""
self.no_param_count += 1
handler = CallbackHandler()
def task(x):
time.sleep(0.05)
return f"task_{x}"
with QThreadPoolExecutor(max_workers=2) as pool:
fut1 = pool.submit(task, 1)
fut1.add_done_callback(handler.handle_result)
fut2 = pool.submit(task, 2)
fut2.add_done_callback(handler.handle_no_param)
fut1.result()
fut2.result()
# 处理Qt事件以确保回调被执行
wait_with_events(200)
assert "task_1" in handler.results, "类方法回调应该接收到值"
assert handler.no_param_count == 1, "无参数类方法回调应该被调用"
def test_pool_failure_callbacks_with_flexible_params(self, qapp):
"""测试线程池失败回调的灵活参数支持"""
error_container = {"called": False, "exception": None}
no_param_container = {"called": False}
def error_callback(exc):
"""带参数的失败回调"""
error_container["called"] = True
error_container["exception"] = exc
def no_param_error_callback():
"""无参数的失败回调"""
no_param_container["called"] = True
def failing_task():
time.sleep(0.05)
raise ValueError("Pool task error")
with QThreadPoolExecutor(max_workers=2) as pool:
# 测试带参数的失败回调
fut1 = pool.submit(failing_task)
fut1.add_failure_callback(error_callback)
with pytest.raises(ValueError):
fut1.result()
wait_with_events(200)
assert error_container["called"], "失败回调应该被调用"
assert isinstance(error_container["exception"], ValueError), (
"应该接收到异常对象"
)
# 测试无参数的失败回调
fut2 = pool.submit(failing_task)
fut2.add_failure_callback(no_param_error_callback)
with pytest.raises(ValueError):
fut2.result()
wait_with_events(200)
assert no_param_container["called"], "无参数失败回调应该被调用"
def test_as_completed_timeout(self, qapp, slow_function):
"""测试as_completed的超时"""
with QThreadPoolExecutor(max_workers=2) as pool:
futs = [pool.submit(slow_function, duration=0.2) for _ in range(2)]
import concurrent.futures
with pytest.raises(concurrent.futures.TimeoutError):
list(QThreadPoolExecutor.as_completed(futs, timeout_ms=10))
@pytest.fixture(scope="session")
def qapp():
"""创建QApplication实例供测试使用"""
if not QApplication.instance():
app = QApplication(sys.argv)
else:
app = QApplication.instance()
yield app
@pytest.fixture(autouse=True)
def test_isolation():
"""自动应用的测试隔离fixture"""
# 测试前清理
app = QApplication.instance()
if app:
# 清理事件队列
app.processEvents()
# 让正在运行的线程有时间完成(减少等待时间)
wait_with_events(10)
yield # 执行测试
# 测试后清理
if app:
# 清理任何残留事件
for _ in range(3):
app.processEvents()
time.sleep(0.001)
# 给线程时间完全清理(减少等待时间)
wait_with_events(10)
@pytest.fixture(autouse=True)
def gradual_cleanup_between_tests():
"""Ensure gradual cleanup between tests to prevent resource accumulation.
CRITICAL FIX: This prevents the SAME gc.collect() crash we fixed in
test_stress_and_concurrency.py and test_production_scenarios.py.
Without this fixture, running the FULL test suite causes:
- Process crash with exit code -1073740791 (0xC0000409) STATUS_STACK_BUFFER_OVERRUN
- Crash occurs at test_thread_really_finished_flag
- Root cause: Simultaneous Qt object deletion during aggressive gc.collect()
This fixture uses the same gradual cleanup pattern that successfully fixed
the other test files.
ADDITIONAL FIX: Increased delay from 300ms to 500ms to handle force_stop scenarios
where multiple threads may be terminating simultaneously and need extra time
for Qt's deleteLater() to complete before gc.collect() runs.
"""
yield
# Allow Qt event loop to process deferred deletions
# Increased from 300ms to 500ms for force_stop cleanup safety
wait_with_events(500) # 500ms for Qt cleanup (was 300ms)
# Gradual garbage collection - only collect youngest generation
# to avoid stack overflow when collecting many Qt objects at once
import gc
gc.collect(generation=0)
@pytest.fixture
def simple_function():
"""简单的测试函数"""
def func(x, y=10):
return x + y
return func
@pytest.fixture
def slow_function():
"""慢速测试函数"""
def func(duration=0.1):
time.sleep(duration)
return "completed"
return func
@pytest.fixture
def error_function():
"""会抛出异常的测试函数"""
def func():
raise ValueError("Test error")
return func
def wait_for_thread_completion(thread, timeout_ms=5000):
"""等待线程完成,包括所有清理工作"""
app = QApplication.instance()
start_time = time.time()
# 首先等待线程完成任务
while not thread.done() and (time.time() - start_time) * 1000 < timeout_ms:
if app:
app.processEvents()
time.sleep(0.01)
# 然后等待线程真正结束
if thread.done():
# 额外等待一段时间确保线程完全清理
additional_wait = 0
while thread.running() and additional_wait < 1000: # 最多额外等待1秒
if app:
app.processEvents()
time.sleep(0.01)
additional_wait += 10
return thread.done()
def wait_with_events(ms):
"""等待指定时间,同时处理Qt事件以允许回调执行
FIXED: Previously just slept without processing events, breaking callback tests.
Now properly processes Qt events to allow deferred cleanup and callback execution.
"""
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is None:
# No Qt app - just sleep
time.sleep(max(0.001, ms / 1000.0))
return
# Qt mode: process events while waiting to allow callbacks to execute
deadline = time.monotonic() + (ms / 1000.0)
while time.monotonic() < deadline:
app.processEvents()
time.sleep(0.010) # 10ms intervals to avoid CPU spinning
class TestQThreadWithReturn:
def test_running_and_done_methods(self, qapp, slow_function):
"""测试 running() 和 done() 方法的行为"""
thread = QThreadWithReturn(slow_function, duration=0.1)
# 初始状态
assert not thread.running()
assert not thread.done()
thread.start()
wait_with_events(20)
# 运行中
assert thread.running()
assert not thread.done()
wait_for_thread_completion(thread, 2000)
# 完成后
assert not thread.running()
assert thread.done()
def test_result_and_exception_success(self, qapp, simple_function):
"""测试 result() 和 exception() 正常返回"""
thread = QThreadWithReturn(simple_function, 1, y=2)
thread.start()
wait_for_thread_completion(thread, 2000)
assert thread.result() == 3
assert thread.exception() is None
def test_result_and_exception_with_timeout(self, qapp, slow_function):
"""测试 result(timeout_ms) 和 exception(timeout_ms) 超时抛出 TimeoutError"""
import concurrent.futures
thread = QThreadWithReturn(slow_function, duration=0.5)
thread.start()
wait_with_events(50)
with pytest.raises(concurrent.futures.TimeoutError):
thread.result(timeout_ms=10)
with pytest.raises(concurrent.futures.TimeoutError):
thread.exception(timeout_ms=10)
wait_for_thread_completion(thread, 2000)
assert thread.result() == "completed"
assert thread.exception() is None
def test_result_and_exception_cancelled(self, qapp, slow_function):
"""测试 result() 和 exception() 被取消时抛 CancelledError"""
import concurrent.futures
thread = QThreadWithReturn(slow_function, duration=0.5)
thread.start()
wait_with_events(50)
thread.cancel()
wait_for_thread_completion(thread, 2000)
with pytest.raises(concurrent.futures.CancelledError):
thread.result()
with pytest.raises(concurrent.futures.CancelledError):
thread.exception()
"""QThreadWithReturn测试类"""
def test_basic_thread_execution_with_return_value(self, qapp, simple_function):
"""测试基本的线程执行和返回值获取"""
# 创建线程
thread = QThreadWithReturn(simple_function, 5, y=15)
# 启动线程
thread.start()
# 等待线程完成
success = wait_for_thread_completion(thread, 5000)
assert success, "Thread should complete within timeout"
# 检查线程已完成
assert thread.done(), "Thread should be finished"
assert not thread.running(), "Thread should not be running"
# 获取结果
result = thread.result()
assert result == 20, f"Expected 20, got {result}"
def test_done_callback_execution(self, qapp, simple_function):
"""测试完成回调函数的执行"""
result_container = {"value": None, "called": False}
def done_callback(result):
result_container["value"] = result
result_container["called"] = True
# 创建线程并设置回调
thread = QThreadWithReturn(simple_function, 10, y=5)
thread.add_done_callback(done_callback)
# 启动线程
thread.start()
# 等待线程完成
success = wait_for_thread_completion(thread, 5000)
assert success, "Thread should complete"
# 额外等待回调执行
wait_with_events(200)
# 验证回调被调用
assert result_container["called"], "Done callback should be called"
assert result_container["value"] == 15, (
f"Expected 15, got {result_container['value']}"
)
def test_failure_callback_execution(self, qapp, error_function):
"""测试失败回调函数的执行"""
error_container = {"exception": None, "called": False}
def failure_callback(exception):
error_container["exception"] = exception
error_container["called"] = True
# 创建线程并设置失败回调
thread = QThreadWithReturn(error_function)
thread.add_failure_callback(failure_callback)
# 启动线程
thread.start()
# 等待线程完成
success = wait_for_thread_completion(thread, 5000)
assert success, "Thread should complete"
# 额外等待回调执行
wait_with_events(200)
# 验证失败回调被调用
assert error_container["called"], "Failure callback should be called"
assert isinstance(error_container["exception"], ValueError), (
"Should receive ValueError"
)
assert str(error_container["exception"]) == "Test error", (
"Exception message should match"
)
# 验证获取结果时抛出异常
with pytest.raises(ValueError, match="Test error"):
thread.result()
def test_thread_cancellation(self, qapp, slow_function):
"""测试线程取消功能"""
# 创建一个较慢的线程
thread = QThreadWithReturn(slow_function, duration=1.0)
# 启动线程
thread.start()
# 等待一小段时间确保线程开始运行
wait_with_events(100)
# 取消线程
cancel_result = thread.cancel()
assert cancel_result, "Thread should be cancellable when running"
# 检查取消状态
assert thread.cancelled(), "Thread should be marked as cancelled"
# 等待线程结束
wait_for_thread_completion(thread, 3000)
# 验证线程状态
assert not thread.running(), "Thread should not be running after cancellation"
def test_timeout_functionality(self, qapp, slow_function):
"""测试超时功能 - 应该触发 TimeoutError 异常"""
exception_captured = []
def on_failure(exc):
exception_captured.append(exc)
# 创建一个会超时的线程(设置较短的超时时间)
thread = QThreadWithReturn(slow_function, duration=1.0)
thread.add_failure_callback(on_failure)
# 启动线程
thread.start(200) # 200ms 超时
# 等待超时发生
wait_with_events(500)
# 验证失败回调被触发并接收到 TimeoutError
assert len(exception_captured) == 1, "Failure callback should be called once"
assert isinstance(exception_captured[0], TimeoutError), "Exception should be TimeoutError"
# 验证线程状态
assert thread.done(), "Thread should be done after timeout"
assert not thread.running(), "Thread should not be running after timeout"
# 验证 exception() 方法返回 TimeoutError
exc = thread.exception(timeout_ms=0)
assert isinstance(exc, TimeoutError), "exception() should return TimeoutError"
# 验证 result() 抛出 TimeoutError
with pytest.raises(TimeoutError):
thread.result(timeout_ms=0)
def test_timeout_triggers_multiple_callbacks(self, qapp):
"""测试超时触发多个失败回调"""
callback_count = []
def callback1(exc):
callback_count.append(1)
assert isinstance(exc, TimeoutError), "callback1 should receive TimeoutError"
def callback2(exc):
callback_count.append(2)
assert isinstance(exc, TimeoutError), "callback2 should receive TimeoutError"
def callback3():
# 无参数回调也应该被触发
callback_count.append(3)
thread = QThreadWithReturn(lambda: time.sleep(2))
thread.add_failure_callback(callback1)
thread.add_failure_callback(callback2)
thread.add_failure_callback(callback3)
thread.start(100) # 100ms 超时
wait_with_events(300)
# 验证所有回调都被触发
assert len(callback_count) == 3, "All three callbacks should be triggered"
assert 1 in callback_count, "callback1 should be called"
assert 2 in callback_count, "callback2 should be called"
assert 3 in callback_count, "callback3 should be called"
def test_callback_parameter_validation(self, qapp):
"""测试回调函数参数验证"""
thread = QThreadWithReturn(lambda: "test")
# 测试正确的回调函数
def correct_callback(result):
pass
# 这应该不会抛出异常
thread.add_done_callback(correct_callback)
thread.add_failure_callback(correct_callback)
# 测试无参数回调(新功能:应该成功)
def no_param_callback():
pass
# 这应该成功(支持无参数回调)
thread.add_done_callback(no_param_callback)
thread.add_failure_callback(no_param_callback)
# 测试多参数回调(需要元组返回值)
def two_params_callback(a, b):
pass
# 这应该成功(支持多参数,但运行时需要匹配的返回值)
thread.add_done_callback(two_params_callback)
thread.add_failure_callback(
two_params_callback
) # 失败回调仍然只传递一个异常参数
def test_comprehensive_callback_parameter_validation(self, qapp):
"""全面测试回调函数参数验证逻辑,确保路径和条件覆盖"""
thread = QThreadWithReturn(lambda: "test")
# 测试1: 正确的单参数回调函数
def correct_callback(result):
pass
# 这应该成功
thread.add_done_callback(correct_callback)
thread.add_failure_callback(correct_callback)
print("✓ 正确的单参数回调函数验证通过")
# 测试2: Lambda表达式带默认参数(你的用例)
test_var = 42
lambda_with_default = lambda result, row=test_var: print(
f"Row {row}, Result: {result}"
)
# 这应该成功(只有一个必需参数result)
thread.add_done_callback(lambda_with_default)
thread.add_failure_callback(lambda_with_default)
print("✓ Lambda带默认参数验证通过")
# 测试3: 多个默认参数
lambda_multiple_defaults = lambda result, a=1, b=2, c=3: print(result, a, b, c)
# 这应该成功(只有一个必需参数result)
thread.add_done_callback(lambda_multiple_defaults)
thread.add_failure_callback(lambda_multiple_defaults)
print("✓ 多个默认参数验证通过")
# 测试4: 无参数函数(新功能:应该成功)
def no_params():
pass
# 现在支持无参数回调,应该成功
thread.add_done_callback(no_params)
thread.add_failure_callback(no_params)
print("✓ 无参数函数现在支持了")
# 测试5: 两个必需参数(新功能:应该成功,支持多参数)
def two_required_params(result, extra):
pass
# 现在支持多参数回调(需要相应的返回值)
thread.add_done_callback(two_required_params)
thread.add_failure_callback(two_required_params)
print("✓ 两个必需参数现在支持了")
# 测试6: 一个必需参数 + 一个默认参数(应该成功)
def one_required_one_default(result, default_param="default"):
pass
thread.add_done_callback(one_required_one_default)
thread.add_failure_callback(one_required_one_default)
print("✓ 一个必需参数 + 一个默认参数验证通过")
# 测试7: *args 参数
def with_args(result, *args):
pass
# 这应该成功(只有一个必需参数result,*args是可变参数)
thread.add_done_callback(with_args)
thread.add_failure_callback(with_args)
print("✓ *args参数验证通过")
# 测试8: **kwargs 参数
def with_kwargs(result, **kwargs):
pass
# 这应该成功(只有一个必需参数result,**kwargs是关键字参数)
thread.add_done_callback(with_kwargs)
thread.add_failure_callback(with_kwargs)
print("✓ **kwargs参数验证通过")
# 测试9: 非可调用对象(应该失败)
with pytest.raises(TypeError, match="must be callable"):
thread.add_done_callback("not_callable")
print("✓ 非可调用对象正确失败")
with pytest.raises(TypeError, match="must be callable"):
thread.add_failure_callback(123)
print("✓ 失败回调非可调用对象正确失败")
# 测试10: 复杂的参数组合
def complex_params(result, a=1, b=2, *args, c=3, **kwargs):
pass
# 这应该成功(只有result是必需参数)
thread.add_done_callback(complex_params)
thread.add_failure_callback(complex_params)
print("✓ 复杂参数组合验证通过")
# 测试11: 只有关键字参数
def keyword_only(result, *, kw_only="default"):
pass
# 这应该成功(只有result是必需参数)
thread.add_done_callback(keyword_only)
thread.add_failure_callback(keyword_only)
print("✓ 只有关键字参数验证通过")
def test_callback_validation_error_handling(self, qapp):
"""测试回调函数验证的错误处理路径"""
thread = QThreadWithReturn(lambda: "test")
# 测试inspect.signature抛出异常的情况
# 创建一个模拟对象,使inspect.signature失败
class MockCallable:
def __call__(self):
pass
def __signature__(self):
raise RuntimeError("Mock signature error")
mock_callable = MockCallable()
# 这应该捕获异常并重新抛出为ValueError
with pytest.raises(ValueError, match="Cannot inspect .* signature"):
thread.add_done_callback(mock_callable)
print("✓ signature检查异常处理正确")
# 测试特殊的lambda表达式
# 带有位置参数和关键字参数的复杂lambda
complex_lambda = lambda result, x, y=10, *args, z=20, **kwargs: None
# 现在支持多参数,应该成功
thread.add_done_callback(complex_lambda)
print("✓ 复杂lambda现在支持了")
def test_lambda_expressions_with_closures(self, qapp):
"""测试带有闭包的lambda表达式(模拟实际使用场景)"""
thread = QThreadWithReturn(lambda: "test result")
# 模拟你的实际使用场景
results = []
# 场景1: 简单的lambda with closure
for i in range(3):
# 使用默认参数捕获循环变量
callback = lambda result, row=i: results.append((row, result))
thread.add_done_callback(callback) # 每次都会覆盖前一个,但验证应该通过
print("✓ 循环中的lambda with closure验证通过")
# 场景2: 更复杂的闭包
def create_callback(index, multiplier=2):
return lambda result, idx=index, mult=multiplier: results.append(
(idx * mult, result)
)
callback = create_callback(5, 3)
thread.add_done_callback(callback)
print("✓ 函数生成的lambda with closure验证通过")
# 场景3: 多层嵌套的默认参数
outer_var = "outer"
callback = lambda result, a=outer_var, b=10, c=lambda x: x * 2: results.append(
(a, b, c(1), result)
)
thread.add_done_callback(callback)
print("✓ 多层嵌套默认参数验证通过")
def test_multiple_thread_instances(self, qapp, simple_function):
"""测试多个线程实例同时运行"""
threads = []
results = []
def create_callback(index):
def callback(result):
results.append((index, result))
return callback
# 创建多个线程
for i in range(3):
thread = QThreadWithReturn(simple_function, i, y=10)
thread.add_done_callback(create_callback(i))
threads.append(thread)
# 启动所有线程
for thread in threads:
thread.start()