Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FileCheck.xs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,10 @@ CODE:
/* is_mocked stays 0 from Newxz */
}
}
/* Reset Perl-level mock state ($_current_mocks, $_last_call_for)
* to match the fresh XS state. Without this, child threads inherit
* stale entries and mock_file_check() croaks "already mocked". */
call_pv("Overload::FileCheck::_clone_init", G_DISCARD);
}

#endif
Expand Down
9 changes: 9 additions & 0 deletions lib/Overload/FileCheck.pm
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,15 @@ sub unmock_all_file_checks {
# and trigger the callback function when mocked
my $_last_call_for;

# Called from XS CLONE to reset Perl-level mock state for the new
# interpreter. The XS layer starts each child thread with all ops
# unmocked (is_mocked = 0), so the Perl-level hash must match.
sub _clone_init {
$_current_mocks = {};
undef $_last_call_for;
return;
}

sub _check {
my ( $optype, $file ) = @_;

Expand Down
71 changes: 71 additions & 0 deletions t/ithreads-clone.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/perl

use strict;
use warnings;

use Config;
use Test2::V0;

BEGIN {
plan skip_all => 'This perl is not built with ithreads support'
unless $Config{useithreads};
}

use threads;
use Overload::FileCheck qw(:check mock_file_check unmock_file_check);

# -- Parent thread mocks -e --------------------------------------------------

mock_file_check(
'-e' => sub {
my ($file) = @_;
return CHECK_IS_TRUE if $file eq '/parent/file';
return FALLBACK_TO_REAL_OP;
}
);

ok( -e '/parent/file', '-e mock works in parent thread' );

# -- Child thread can re-mock independently -----------------------------------

my $thr = threads->create(sub {
# The XS CLONE resets is_mocked=0, and _clone_init clears
# $_current_mocks. So the child should be able to mock -e
# without getting "already mocked" error.

my $can_mock = eval {
mock_file_check(
'-e' => sub {
my ($file) = @_;
return CHECK_IS_TRUE if $file eq '/child/file';
return FALLBACK_TO_REAL_OP;
}
);
1;
};

my $mock_error = $@;
my $child_works = $can_mock ? ( -e '/child/file' ? 1 : 0 ) : 0;

# Parent's mock should not be active in child
my $parent_leaked = -e '/parent/file' ? 1 : 0;

unmock_file_check('-e') if $can_mock;

return ( $can_mock, $mock_error, $child_works, $parent_leaked );
});

my ( $can_mock, $mock_error, $child_works, $parent_leaked ) = $thr->join;

ok( $can_mock, 'child thread can mock_file_check without "already mocked" error' )
or diag("mock error: $mock_error");
ok( $child_works, 'child thread mock returns correct value' );
ok( !$parent_leaked, 'parent mock state does not leak into child thread' );

# -- Parent mock still works after child exits --------------------------------

ok( -e '/parent/file', 'parent mock unaffected by child thread' );

unmock_file_check('-e');

done_testing;
Loading