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 1/3] 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; From 04e2c3a79092f462080f74b60b369588bed81cf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Fri, 15 May 2026 06:39:15 -0600 Subject: [PATCH 2/3] fix: use lazy XS state check instead of call_pv during CLONE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach called call_pv("_clone_init") from XS CLONE to reset Perl-level mock state, but call_pv during CLONE is unreliable on some Perl builds (ubuntu CI failure). New approach: add _xs_is_mocked() query function and make mock_file_check() itself resilient — when it finds a stale entry in $_current_mocks but XS says the op is not mocked, it clears the stale entry and proceeds. This works regardless of CLONE timing. Co-Authored-By: Claude Opus 4.6 --- FileCheck.xs | 29 +++++++++++++++++++++++++---- lib/Overload/FileCheck.pm | 20 ++++++++++---------- 2 files changed, 35 insertions(+), 14 deletions(-) diff --git a/FileCheck.xs b/FileCheck.xs index 0b0f127..ce88147 100644 --- a/FileCheck.xs +++ b/FileCheck.xs @@ -532,6 +532,27 @@ mock_op(optype) RETVAL +int +_xs_is_mocked(optype) + SV* optype; +CODE: +{ + dMY_CXT; + int opid; + + if ( ! SvIOK(optype) ) + croak("first argument to _xs_is_mocked must be one integer"); + + opid = SvIV( optype ); + if ( !opid || opid < 0 || opid >= OP_MAX ) + croak( "Invalid opid value %d", opid ); + + RETVAL = gl_overload_ft->op[opid].is_mocked; +} +OUTPUT: + RETVAL + + SV* get_basetime() CODE: @@ -650,10 +671,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); + /* Perl-level state ($_current_mocks) is reset lazily in + * mock_file_check() via _xs_is_mocked() check, rather than + * here, because call_pv() during CLONE is unreliable on + * some Perl builds. */ } #endif diff --git a/lib/Overload/FileCheck.pm b/lib/Overload/FileCheck.pm index 66aff0c..a745965 100644 --- a/lib/Overload/FileCheck.pm +++ b/lib/Overload/FileCheck.pm @@ -235,7 +235,16 @@ sub mock_file_check { Carp::croak(q[Second arg must be a CODE ref]) unless ref $sub eq 'CODE'; my ( $name, $optype ) = _resolve_check($check); - Carp::croak(qq[-$name is already mocked by Overload::FileCheck]) if exists $_current_mocks->{$optype}; + + if ( exists $_current_mocks->{$optype} ) { + # In ithreads, the Perl hash may carry stale entries from the + # parent thread while the XS layer has already reset is_mocked. + # Check the actual XS state before croaking. + if ( _xs_is_mocked($optype) ) { + Carp::croak(qq[-$name is already mocked by Overload::FileCheck]); + } + delete $_current_mocks->{$optype}; + } $_current_mocks->{$optype} = $sub; @@ -459,15 +468,6 @@ 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 ) = @_; From 02ab7802a284813901fb001426ccfc15a44f0a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C5=8Dan?= Date: Fri, 15 May 2026 06:35:09 -0600 Subject: [PATCH 3/3] ci: add cpanm fallback when cpm is unavailable Older perldocker/perl-tester images (5.10-5.22) no longer ship cpm pre-installed, causing CI failures on all PR branches. Fall back to cpanm --installdeps when cpm is not found. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/testsuite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 8af84cc..b3673de 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -69,7 +69,7 @@ jobs: - uses: actions/checkout@v6 - run: perl -V - name: Install Dependencies - run: cpm install -g --show-build-log-on-failure + run: cpm install -g --show-build-log-on-failure || cpanm --installdeps --notest . - run: perl Makefile.PL - run: make - run: make test