Algorithm-Classifier-IsolationForest

 view release on metacpan or  search on metacpan

t/90-cli-commands.t  view on Meta::CPAN

#!perl
# 90-cli-commands.t
#
# Smoke-tests the iforest CLI subcommands by running bin/iforest in a
# subprocess against a temp model + CSV.  We chain them through a
# realistic workflow:
#
#   fit  -> info
#        -> bench
#        -> pack -> predict (packed input)
#        -> predict (raw CSV input, for regression baseline)
#
# Each subtest checks the relevant artefacts and output snippets but
# stays loose on the exact wording -- we want to catch breakage, not
# pin the formatting.

use strict;
use warnings;
use Test::More;
use File::Temp qw(tempdir);
use File::Spec;

my $bin = File::Spec->rel2abs('bin/iforest');
plan skip_all => "bin/iforest not found" unless -x $bin;

my $tmp = tempdir( CLEANUP => 1 );

# --- 1. Generate a tiny CSV training dataset ----------------------------
my $train_csv = "$tmp/train.csv";
my $query_csv = "$tmp/query.csv";
my $model     = "$tmp/model.json";
my $packed    = "$tmp/query.packed";
my $pred_csv  = "$tmp/pred.csv";
my $pred2_csv = "$tmp/pred2.csv";

{
	open my $fh, '>', $train_csv or die $!;
	# 50 inliers around the origin + 3 outliers far away (3 features).
	srand(1);
	for ( 1 .. 50 ) {
		print $fh join( ',', map { sprintf( '%.4f', rand() - 0.5 ) } 1 .. 3 ), "\n";
	}
	print $fh "8,8,8\n-7,-7,-7\n7,-7,7\n";
	close $fh;
}
{
	open my $fh, '>', $query_csv or die $!;
	# 5 inliers + 2 outliers.
	srand(2);
	for ( 1 .. 5 ) {
		print $fh join( ',', map { sprintf( '%.4f', rand() - 0.5 ) } 1 .. 3 ), "\n";
	}
	print $fh "9,9,9\n-8,-8,-8\n";
	close $fh;
}

# --- 2. fit (a known-good command) --------------------------------------
subtest 'fit produces a model' => sub {
	my $out = `$^X -Ilib $bin fit -i $train_csv -o $model -n 30 -m 16 -s 42 2>&1`;
	is( $?, 0, 'fit exits 0' );
	ok( -s $model, 'model.json was written' );
};

# --- 3. info ------------------------------------------------------------
subtest 'info dumps model metadata' => sub {
	my $out = `$^X -Ilib $bin info -m $model 2>&1`;
	is( $?, 0, 'info exits 0' );
	like( $out, qr/n_trees\s+30/,           'info reports n_trees=30' );
	like( $out, qr/n_features\s+3/,         'info reports n_features=3' );
	like( $out, qr/mode\s+axis/,            'info reports mode=axis' );
	like( $out, qr/tree_total_nodes\s+\d+/, 'info reports a tree_total_nodes count' );
};

subtest 'info --json emits parseable JSON' => sub {
	my $out = `$^X -Ilib $bin info -m $model --json 2>&1`;
	is( $?, 0, 'info --json exits 0' );
	require JSON::PP;
	my $obj = eval { JSON::PP->new->decode($out) };
	ok( !$@, 'output parses as JSON' ) or diag("error: $@");
	is( $obj->{n_trees}, 30,     'JSON n_trees matches' ) if $obj;
	is( $obj->{mode},    'axis', 'JSON mode matches' )    if $obj;
};

# --- 3b. info reports feature-name tags --------------------------------
subtest 'info displays tag info' => sub {
	# The baseline model was fit without -t, so it is untagged.
	my $out = `$^X -Ilib $bin info -m $model 2>&1`;
	like( $out, qr/tagged\s+0/, 'untagged model reports tagged=0' );
	unlike( $out, qr/feature_names/, 'untagged model omits the feature_names block' );

	# Fit a tagged model over the same 3-feature CSV.
	my $tagged = "$tmp/tagged.json";
	my $fit    = `$^X -Ilib $bin fit -i $train_csv -o $tagged -n 30 -m 16 -s 42 -t cpu -t mem -t disk 2>&1`;
	is( $?, 0, 'tagged fit exits 0' ) or diag($fit);

	my $t = `$^X -Ilib $bin info -m $tagged 2>&1`;
	is( $?, 0, 'info on tagged model exits 0' );
	like( $t, qr/tagged\s+1/,                     'reports tagged=1' );



( run in 0.886 second using v1.01-cache-2.11-cpan-9581c071862 )