From 26a1de518af1a38df98db8b782c5726e5de23883 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Mon, 4 May 2026 06:03:26 -0600 Subject: [PATCH] fix: reset Perl-level mock state in ithreads CLONE The XS CLONE handler correctly starts child threads with all ops unmocked (is_mocked=0), but the Perl-level $_current_mocks hash was not cleared. This caused mock_file_check() in child threads to croak "already mocked" for ops mocked in the parent. Add _clone_init() called from XS CLONE to reset $_current_mocks and $_last_call_for. Add ithreads-clone.t (skips on non-threaded perls). Co-Authored-By: Claude --- FileCheck.xs | 4 +++ lib/Overload/FileCheck.pm | 9 +++++ t/ithreads-clone.t | 71 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 t/ithreads-clone.t diff --git a/FileCheck.xs b/FileCheck.xs index 8d893fc..0b0f127 100644 --- a/FileCheck.xs +++ b/FileCheck.xs @@ -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 diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 5034976..66aff0c 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -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 ) = @_; diff --git a/t/ithreads-clone.t b/t/ithreads-clone.t new file mode 100644 index 0000000..12391f2 --- /dev/null +++ b/t/ithreads-clone.t @@ -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;