3737 % % Async event loop dispatch test
3838 async_receive_bytes_e2e_test /1 ,
3939 % % create_task + async receive test
40- create_task_async_receive_test /1
40+ create_task_async_receive_test /1 ,
41+ % % Close and drain tests
42+ close_drain_bytes_erlang_test /1 ,
43+ close_drain_bytes_python_sync_test /1 ,
44+ close_drain_bytes_python_async_test /1
4145]).
4246
4347all () -> [
@@ -64,7 +68,11 @@ all() -> [
6468 % % Async event loop dispatch test
6569 async_receive_bytes_e2e_test ,
6670 % % create_task + async receive test
67- create_task_async_receive_test
71+ create_task_async_receive_test ,
72+ % % Close and drain tests
73+ close_drain_bytes_erlang_test ,
74+ close_drain_bytes_python_sync_test ,
75+ close_drain_bytes_python_async_test
6876].
6977
7078init_per_suite (Config ) ->
@@ -428,3 +436,97 @@ async def task_receive_bytes(ch_ref, reply_pid):
428436 end ,
429437
430438 ok = py_byte_channel :close (Ch ).
439+
440+ % %% ============================================================================
441+ % %% Close and Drain Tests
442+ % %% ============================================================================
443+
444+ % % @doc Test that data can be drained from byte channel after close (Erlang side)
445+ % % Verifies that closing a channel doesn't prevent reading existing data
446+ close_drain_bytes_erlang_test (_Config ) ->
447+ {ok , Ch } = py_byte_channel :new (),
448+
449+ % % Send multiple messages
450+ ok = py_byte_channel :send (Ch , <<" chunk1" >>),
451+ ok = py_byte_channel :send (Ch , <<" chunk2" >>),
452+ ok = py_byte_channel :send (Ch , <<" chunk3" >>),
453+
454+ % % Close the channel while data is still in queue
455+ ok = py_byte_channel :close (Ch ),
456+
457+ % % Verify channel is marked as closed
458+ Info = py_byte_channel :info (Ch ),
459+ true = maps :get (closed , Info ),
460+
461+ % % Should still be able to drain all data
462+ {ok , <<" chunk1" >>} = py_byte_channel :try_receive (Ch ),
463+ {ok , <<" chunk2" >>} = py_byte_channel :try_receive (Ch ),
464+ {ok , <<" chunk3" >>} = py_byte_channel :try_receive (Ch ),
465+
466+ % % Only after draining should we get closed error
467+ {error , closed } = py_byte_channel :try_receive (Ch ),
468+
469+ ct :pal (" Erlang byte channel close+drain test passed" ).
470+
471+ % % @doc Test that Python can drain data from closed byte channel (sync iteration)
472+ close_drain_bytes_python_sync_test (_Config ) ->
473+ {ok , Ch } = py_byte_channel :new (),
474+
475+ % % Send multiple messages
476+ ok = py_byte_channel :send (Ch , <<" data1" >>),
477+ ok = py_byte_channel :send (Ch , <<" data2" >>),
478+ ok = py_byte_channel :send (Ch , <<" data3" >>),
479+
480+ % % Close the channel
481+ ok = py_byte_channel :close (Ch ),
482+
483+ % % Python should be able to drain all messages via iteration
484+ Ctx = py :context (1 ),
485+ ok = py :exec (Ctx , <<"
486+ from erlang import ByteChannel, ByteChannelClosed
487+
488+ def drain_byte_channel(ch_ref):
489+ '''Drain all chunks from byte channel, return as list.'''
490+ ch = ByteChannel(ch_ref)
491+ chunks = []
492+ for chunk in ch:
493+ chunks.append(chunk)
494+ return chunks
495+ " >>),
496+
497+ {ok , [<<" data1" >>, <<" data2" >>, <<" data3" >>]} =
498+ py :eval (Ctx , <<" drain_byte_channel(ch)" >>, #{<<" ch" >> => Ch }),
499+
500+ ct :pal (" Python sync byte channel close+drain test passed" ).
501+
502+ % % @doc Test that Python can drain data from closed byte channel (async iteration)
503+ close_drain_bytes_python_async_test (_Config ) ->
504+ {ok , Ch } = py_byte_channel :new (),
505+
506+ % % Send multiple messages
507+ ok = py_byte_channel :send (Ch , <<" part1" >>),
508+ ok = py_byte_channel :send (Ch , <<" part2" >>),
509+ ok = py_byte_channel :send (Ch , <<" part3" >>),
510+
511+ % % Close the channel
512+ ok = py_byte_channel :close (Ch ),
513+
514+ % % Python should be able to drain all messages via async iteration
515+ Ctx = py :context (1 ),
516+ ok = py :exec (Ctx , <<"
517+ import erlang
518+ from erlang import ByteChannel, ByteChannelClosed
519+
520+ async def async_drain_byte_channel(ch_ref):
521+ '''Async drain all chunks from byte channel, return as list.'''
522+ ch = ByteChannel(ch_ref)
523+ chunks = []
524+ async for chunk in ch:
525+ chunks.append(chunk)
526+ return chunks
527+ " >>),
528+
529+ {ok , [<<" part1" >>, <<" part2" >>, <<" part3" >>]} =
530+ py :eval (Ctx , <<" erlang.run(async_drain_byte_channel(ch))" >>, #{<<" ch" >> => Ch }),
531+
532+ ct :pal (" Python async byte channel close+drain test passed" ).
0 commit comments