Acme-Parataxis

 view release on metacpan or  search on metacpan

t/028_stress_fuzz.t  view on Meta::CPAN

    my $sig  = Acme::Parataxis::Signal->new;
    my $chan = Acme::Parataxis::Channel->new( capacity => 1 );
    my %inv;
    my @fibers;
    for ( 1 .. $plan{S} ) {
        push @fibers, fiber { await_sleep( 1 + $rand->(5) ) };
    }
    for ( 1 .. $plan{G} ) {
        push @fibers, fiber {
            my $g = $sem->guard;
            yield for 1 .. 2;
            undef $g;
            $inv{guards}++;
        };
    }
    my $put_seq = 0;
    for ( 1 .. $plan{P} ) {
        push @fibers, fiber { $chan->put( ++$put_seq ) };
    }
    for ( 1 .. $plan{P} ) {
        push @fibers, fiber {
            my $v = $chan->get;
            push @{ $inv{tokens} }, $v;
        };
    }
    for ( 1 .. $plan{W} ) {
        push @fibers, fiber { $sig->wait; $inv{signalled}++ };
    }
    for ( 1 .. $plan{A} ) {
        push @fibers, fiber { my $v = $fut->await; $inv{awaited}++; $inv{fut_value} = $v };
    }

    # Drop the Perl reference to a random subset while they are still parked: the C
    # context keeps them alive, but this exercises destroy-without-a-Perl-reference.
    undef $fibers[$_] for grep { $rand->(5) == 0 } 0 .. $#fibers;
    undef @fibers;

    # Interleave random operations, sprinkling in the balanced sends and guaranteeing
    # every planned send actually happens.
    my $need_ups = $plan{G};
    my $need_sig = $plan{W};
    my ( $ups_done, $fut_set ) = ( 0, 0 );
    for ( 1 .. 20 + $rand->(40) ) {
        my $op = $rand->(8);
        if    ( $op == 0 && $need_ups )             { $sem->up;   $need_ups-- }
        elsif ( $op == 1 && $need_sig )             { $sig->send; $need_sig-- }
        elsif ( $op == 2 )                          { $sem->up }
        elsif ( $op == 3 )                          { $sig->send }
        elsif ( $op == 4 && !$fut_set && $plan{A} ) { $fut->set_result('v'); $fut_set = 1 }
        elsif ( $op == 5 )                          { await_sleep(1) }
        elsif ( $op == 6 )                          {yield}
        else                                        {maybe_yield}
    }
    $sem->up   for 1 .. $need_ups;
    $sig->send for 1 .. $need_sig;
    $fut->set_result('v') if $plan{A} && !$fut_set;
    maybe_yield for 1 .. 3;
    return { plan => \%plan, inv => \%inv };
}

# Prologue: abort a run mid-flight so a parked fiber and its job are left over; every subsequent iteration has to drain
# and reclaim that leftover while running its own population. This is the cross-run contamination case from the
# hardening pass.
async {
    my $leaker = fiber { await_sleep(4) };
    yield for 1 .. 2;
    stop;
};
my ( $pass, $fails ) = ( 0, 0 );
my @problems_out;
for my $iter ( 1 .. $ITERS ) {
    my $seed = $BASE_SEED + $iter;
    my ( $out, $err );
    eval {
        $out = async { one_iteration($seed) }
    };
    if ($@)                   { @problems_out = ( @problems_out, "iter $iter (seed $seed) croak escaped: $@" );      next }
    if ( ref $out ne 'HASH' ) { @problems_out = ( @problems_out, "iter $iter (seed $seed) async returned nothing" ); next }
    my $plan = $out->{plan};
    my $inv  = $out->{inv};
    my @p;
    push @p, 'fibers left behind (a clean run must exit with zero)'                 if live_count() != 0;
    push @p, 'jobs left outstanding after run'                                      if outstanding() != 0;
    push @p, "lost semaphore wake: $inv->{guards} guards ran (expected $plan->{G})" if $inv->{guards} != $plan->{G};
    push @p, "lost future wake: $inv->{awaited} awaiters (expected $plan->{A})"     if $plan->{A} && $inv->{awaited} != $plan->{A};
    push @p, "future misdelivered value $inv->{fut_value} (expected v)"             if $plan->{A} && ( $inv->{fut_value} // '' ) ne 'v';
    push @p, "lost signal: $inv->{signalled} waiters woken (expected $plan->{W})"   if $inv->{signalled} != $plan->{W};
    my $tokens = join ',', sort { $a <=> $b } @{ $inv->{tokens} // [] };
    push @p, "channel tokens [$tokens] != 1..$plan->{P}" if $tokens ne join( ',', 1 .. $plan->{P} );

    if (@p) {
        $fails++;
        @problems_out = (
            @problems_out,
            'iter ' . $iter . " (seed $seed; " . join( ' ', map {"$_=$plan->{$_}"} sort keys %{ $plan // {} } ) . "):\n  " . join "\n  ", @p
        );
    }
    else {
        $pass++;
    }
}
ok $fails == 0, "stress fuzz: $pass/$ITERS clean iterations";
note join "\n", @problems_out if @problems_out;
#
done_testing;



( run in 0.994 second using v1.01-cache-2.11-cpan-eca53f54451 )