diff --git a/.gitignore b/.gitignore index 07d5e7f..777b362 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ pm_to_blib Test-MockFile-* Test-MockFile-*.tar.gz .DS_Store +.claude/ # VIM - https://github.com/github/gitignore/blob/main/Global/Vim.gitignore # Swap diff --git a/Makefile.PL b/Makefile.PL index e6935d5..e4433c0 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -26,7 +26,7 @@ WriteMakefile( 'Test::MockModule' => 0, }, PREREQ_PM => { - 'Overload::FileCheck' => '0.013', + 'Overload::FileCheck' => '0.014', 'Text::Glob' => 0, }, dist => { COMPRESS => 'gzip -9f', SUFFIX => 'gz', }, diff --git a/cpanfile b/cpanfile index 5a9ec29..4e19f2a 100644 --- a/cpanfile +++ b/cpanfile @@ -13,7 +13,7 @@ on 'test' => sub { requires 'Test2::Tools::Explain' => 0; requires 'Test2::Plugin::NoWarnings' => 0; requires 'File::Slurper' => 0; - requires 'Overload::FileCheck' => '0.013'; + requires 'Overload::FileCheck' => '0.014'; requires 'Test::Pod::Coverage' => 0; requires 'Test::Pod' => 0; requires 'Test2::API' => 0; diff --git a/t/fh-ref-leak.t b/t/fh-ref-leak.t new file mode 100644 index 0000000..a32310c --- /dev/null +++ b/t/fh-ref-leak.t @@ -0,0 +1,90 @@ +#!/usr/bin/perl -w + +# Test for GitHub issue #179: "Spooky action-at-a-distance" +# +# File check operators (-S, -f, etc.) on real (unmocked) filehandles should +# not retain references that prevent garbage collection. A leaked reference +# to a socket filehandle can keep the fd open, causing reads on the other +# end of a socketpair to hang waiting for EOF. +# +# Root cause: $_last_call_for in Overload::FileCheck stored filehandle refs. +# Fix: Only cache string filenames, not refs (Overload::FileCheck PR #25). + +use strict; +use warnings; + +use Test2::Bundle::Extended; +use Test2::Tools::Explain; + +use Scalar::Util qw(weaken); +use Socket; + +use Test::MockFile qw< nostrict >; + +# Test 1: Filehandle passed to -f is not retained +{ + my $weak_ref; + + { + open my $fh, '<', '/dev/null' or die "Cannot open /dev/null: $!"; + $weak_ref = $fh; + weaken($weak_ref); + + ok( defined $weak_ref, "weak ref is defined before scope exit" ); + + no warnings; + -f $fh; + } + + ok( !defined $weak_ref, "filehandle is garbage collected after -f (GH #179)" ); +} + +# Test 2: Socket filehandle passed to -S is not retained +{ + my $weak_ref; + + { + open my $fh, '<', '/dev/null' or die "Cannot open /dev/null: $!"; + $weak_ref = $fh; + weaken($weak_ref); + + no warnings; + -S $fh; + } + + ok( !defined $weak_ref, "filehandle is garbage collected after -S (GH #179)" ); +} + +# Test 3: The exact scenario from GH #179 — socketpair with dup'd fd +# This would hang without the fix because the dup'd write handle stays open. +{ + socketpair my $r, my $w, AF_UNIX, SOCK_STREAM, 0 + or die "socketpair: $!"; + + my $pid = fork(); + die "fork: $!" unless defined $pid; + + if ( $pid == 0 ) { + # Child: reproduce the bug scenario with a timeout + $SIG{ALRM} = sub { exit 1 }; # exit 1 = hung (bug present) + alarm(5); + + my $fd = fileno $w; + do { + open my $w2, "<&=", $fd; + -S $w2; + }; + + close $w; + my $line = <$r>; # Should get EOF immediately if $w2 was freed + exit 0; # exit 0 = success (no hang) + } + + close $w; + waitpid $pid, 0; + my $exit = $? >> 8; + + is( $exit, 0, "socketpair read does not hang after -S on dup'd filehandle (GH #179)" ); +} + +done_testing;