Algorithm-Classifier-IsolationForest
view release on metacpan or search on metacpan
t/03-fit-determinism.t view on Meta::CPAN
#!perl
# 03-fit-determinism.t
#
# Verifies that fit() with a given `seed` produces reproducible trees,
# across every tree-building backend the module can select:
#
# * pure Perl (use_c => 0)
# * serial C (use_c => 1)
# * OpenMP-parallel C (use_c => 1, use_openmp_fit => 1)
#
# Covers:
# 1. Each backend, run twice with the same seed, builds bit-identical
# trees (not just bit-identical scores -- the actual tree structure).
# 2. Pure-Perl and serial-C build BIT-IDENTICAL trees for the same seed,
# in both axis and extended mode, and across every `missing`
# strategy -- because the serial C builder draws randomness through
# Drand01(), the same generator Perl's own rand()/srand() use.
# 3. use_openmp_fit is reproducible for a fixed seed + n_trees
# regardless of OMP_NUM_THREADS (checked across separate
# subprocesses, since that's the only way to force a different
# thread count) -- it deliberately does NOT match the Drand01-based
# backends (documented behaviour: it uses its own thread-safe PRNG),
# so that's checked as a "differs, but each is internally consistent"
# property rather than cross-backend equality.
# 4. use_openmp_fit + parallel_fit together is still safe and
# reproducible -- but does NOT actually run forked workers in
# OpenMP. A forked child starting its own OpenMP region after
# the parent process has used OpenMP for anything (including
# plain score_samples()) can hang -- a general fork()+libgomp
# limitation -- so parallel_fit's workers always use the
# single-threaded C builder regardless of use_openmp_fit. This
# was caught by an earlier version of this test hanging.
use strict;
use warnings;
use Test::More;
use File::Temp qw(tempfile);
use Config;
use JSON::PP ();
use Algorithm::Classifier::IsolationForest;
my $CLASS = 'Algorithm::Classifier::IsolationForest';
my $HAS_C = $Algorithm::Classifier::IsolationForest::HAS_C ? 1 : 0;
my $HAS_OPENMP = $Algorithm::Classifier::IsolationForest::HAS_OPENMP ? 1 : 0;
my $can_fork = ( $Config{d_fork} || '' ) eq 'define';
# ------------------------------------------------------------------------
# Shared deterministic dataset. A few obvious outliers keep both axis
# and extended splits from degenerating on featureless data.
# ------------------------------------------------------------------------
srand(20260701);
my @data;
push @data, [ rand(), rand(), rand() ] for 1 .. 150;
push @data, [ 9, 9, 9 ], [ -9, -9, -9 ], [ 8, -9, 8 ];
# A copy with a few undef cells, for the missing-strategy comparisons.
my @data_nan = map { [@$_] } @data;
$data_nan[3][1] = undef;
$data_nan[40][0] = undef;
sub trees_json {
my ($f) = @_;
return JSON::PP->new->canonical(1)->encode( $f->{trees} );
}
sub fit_trees {
my (%opts) = @_;
my $data = delete $opts{data} // \@data;
return trees_json( $CLASS->new(%opts)->fit($data) );
}
# Bounds a block with alarm() so a regression of the fork()+libgomp
# hazard documented below fails this one assertion instead of hanging
# the whole test run (and anything waiting on it, e.g. CI) forever.
sub with_timeout {
my ( $secs, $code ) = @_;
my $result;
eval {
local $SIG{ALRM} = sub { die "timeout\n" };
alarm($secs);
$result = $code->();
alarm(0);
};
alarm(0);
return ( $result, $@ );
} ## end sub with_timeout
( run in 1.408 second using v1.01-cache-2.11-cpan-6aa56a78535 )