Class-Abstract

 view release on metacpan or  search on metacpan

t/edge_cases.t  view on Meta::CPAN

#!/usr/bin/perl
# t/edge_cases.t -- destructive, boundary-condition, and security tests.
#
# Deliberately tries to break or subvert Class::Abstract by passing pathological
# inputs, mocking upstream dependencies with edge-case returns, manipulating
# @ISA at runtime, exploiting bypass mechanics, and verifying robustness under
# circular data structures.

use strict;
use warnings;

use Readonly;
use Scalar::Util     qw(blessed reftype weaken);
use Test::Most;
use Test::Mockingbird;
use Test::Returns;

use Class::Abstract;

# ---------------------------------------------------------------------------
# Configuration -- every constant and boundary value in one place
# ---------------------------------------------------------------------------

my %config = (
	# Package names (EC:: prefix avoids collisions with other test files)
	pkg_abstract	=> 'EC::Abstract',
	pkg_concrete	=> 'EC::Concrete',
	pkg_dynamic	=> 'EC::Dynamic',
	pkg_circ_a	=> 'EC::CircA',
	pkg_circ_b	=> 'EC::CircB',
	pkg_module	=> 'Class::Abstract',

	# Boundary string values -- chosen to probe the defined() / length() guards
	str_zero	=> '0',          # falsy but length 1; must be treated as valid
	str_spaces	=> '   ',        # whitespace-only; length 3 but not a valid identifier
	str_long	=> 'A' x 5_000,  # very long identifier; no length limit in spec
	str_numeric	=> '123',        # numeric string; valid Perl package name

	# Truthy non-boolean bypass values (documented in the POD warnings section)
	bypass_str_false	=> 'false',  # counterintuitive but truthy
	bypass_str_zero_e	=> '0E0',    # numerically 0 but string-truthy
	bypass_float	=> 0.1,          # truthy number
	bypass_ref	=> [],             # reference; truthy

	# Expected error patterns matching the module POD exactly
	err_new_unblessed	=> qr/new\(\) invocant must be a class name or blessed object, got/,
	err_new_undef		=> qr/new\(\) requires a defined class name as invocant/,
	err_chk_unblessed	=> qr/check_abstract\(\) requires a class name or blessed object/,
	err_chk_undef		=> qr/check_abstract\(\) requires a defined class name/,
	err_isabs_undef		=> qr/is_abstract\(\) requires a class name or object invocant/,
	err_abstract		=> qr/Cannot instantiate abstract class \S+ directly/,

	# Test::Returns schemas
	schema_string	=> { type => 'string'  },
	schema_integer	=> { type => 'integer' },
);

# Readonly constants -- prevent accidental mutation
Readonly::Scalar my $CLASS_ABSTRACT	=> 'Class::Abstract';
Readonly::Scalar my $TRUE		=> 1;
Readonly::Scalar my $FALSE		=> 0;

# ---------------------------------------------------------------------------
# Fixture packages for edge-case scenarios
# ---------------------------------------------------------------------------

# EC::Abstract: directly abstract via use parent
{
	package EC::Abstract;
	use parent -norequire, 'Class::Abstract';
}

# EC::Concrete: concrete subclass of EC::Abstract

t/edge_cases.t  view on Meta::CPAN


# Purpose: document that enforcement has no fallback if croak is neutralized
subtest 'mock croak as no-op -- enforcement is defeated (design limitation)' => sub {
	plan tests => 2;

	# Replace croak with a silent no-op that returns without dying
	mock 'Class::Abstract::croak' => sub { return };

	enforcement_on {
		# EC::Abstract->new() would normally croak, but the mock prevents it
		my $obj = EC::Abstract->new();

		restore_all();

		# With croak neutralized, execution continues to 'return bless {}, $class'
		ok blessed($obj),
			'SECURITY: neutralized croak lets abstract class be instantiated';

		is ref($obj), $config{pkg_abstract},
			'SECURITY: resulting object is blessed into the abstract class';
	};
};

# ===========================================================================
# SECTION 14: Context sensitivity for is_abstract()
#
# is_abstract() returns a scalar integer.  In list context it must still
# deliver a single meaningful value.  In boolean context the result must
# be truthy for abstract classes and falsy for concrete ones.
# ===========================================================================

# Purpose: is_abstract() behaves consistently across calling contexts
subtest 'is_abstract() -- consistent in list, scalar, and boolean contexts' => sub {
	plan tests => 5;

	# List context: must return a list of exactly one element
	my @list = EC::Abstract->is_abstract();
	is scalar @list, 1,
		'is_abstract() in list context returns a 1-element list';

	is $list[0], $TRUE,
		'is_abstract() list context: first element is 1 for abstract class';

	# Scalar context: must return the integer 1 for abstract class
	my $scalar = EC::Abstract->is_abstract();
	is $scalar, $TRUE,
		'is_abstract() in scalar context returns 1 for abstract class';

	# Boolean context: abstract must be truthy, concrete must be falsy.
	# Explicit parens prevent 'ok CLASS->method' being parsed as 'CLASS->ok(...)'.
	ok( EC::Abstract->is_abstract(),
		'EC::Abstract->is_abstract() is truthy in boolean context' );

	ok( !EC::Concrete->is_abstract(),
		'EC::Concrete->is_abstract() is falsy in boolean context' );
};

# ===========================================================================
# SECTION 15: Weakened reference as invocant
#
# Scalar::Util::weaken() removes the strong reference count.  Once all
# strong references are gone the object is destroyed and the weak ref becomes
# undef.  A live (still-referenced) weakened blessed ref must pass new().
# A dead (garbage-collected) weakened ref becomes undef and must croak.
# ===========================================================================

# Purpose: live weakened blessed ref works; dead weakened ref croaks
subtest 'weakened references as invocants' => sub {
	plan tests => 3;

	# Live weak ref: a strong ref also exists, so the object survives
	my $strong = EC::Concrete->new();
	my $weak   = $strong;
	weaken($weak);

	diag 'weak ref: ' . (defined($weak) ? ref($weak) : 'undef') if $ENV{TEST_VERBOSE};

	# $weak is still alive because $strong holds a strong reference
	ok defined($weak) && blessed($weak),
		'precondition: weakened ref is still alive (strong ref holds it)';

	# A live weakened blessed ref must be accepted by new()
	lives_ok { $strong->new() }
		'live weakened blessed ref: new() succeeds via strong ref';

	# Dead weak ref: no strong references, object is garbage-collected immediately
	my $dead_weak = do { my $obj = EC::Concrete->new(); weaken($obj); $obj };

	# The dead weak ref is undef; ref(undef) = "", so it hits the undef guard
	throws_ok { Class::Abstract::new($dead_weak) }
		$config{err_new_undef},
		'dead weakened ref (undef) causes new() to croak with defined-class-name error';
};

# ===========================================================================
# SECTION 16: $_ is never clobbered by any public method
#
# Any grep or map over @ISA inside _is_direct_abstract could overwrite $_.
# Verify $_ is unchanged across all four public methods under varied inputs.
# ===========================================================================

# Purpose: no public method may mutate $_ in the calling scope
subtest '$_ not clobbered by any public method under varied inputs' => sub {
	plan tests => 4;

	# Use a value containing characters that would cause obvious corruption
	Readonly::Scalar my $SENTINEL => 'SENTINEL_DO_NOT_MUTATE';

	# import() must leave $_ alone
	{
		local $_ = $SENTINEL;
		{ package EC::DontClobberImport; our @ISA = (); Class::Abstract->import() }
		is $_, $SENTINEL, '$_ unchanged after import()';
	}

	# new() must leave $_ alone
	{
		local $_ = $SENTINEL;
		EC::Concrete->new();
		is $_, $SENTINEL, '$_ unchanged after new()';
	}

	# check_abstract() must leave $_ alone
	{
		local $_ = $SENTINEL;
		enforcement_on { Class::Abstract::check_abstract($config{pkg_concrete}) };
		is $_, $SENTINEL, '$_ unchanged after check_abstract()';
	}

	# is_abstract() must leave $_ alone (grep on @ISA is the risk)
	{
		local $_ = $SENTINEL;
		EC::Abstract->is_abstract();
		EC::Concrete->is_abstract();
		Class::Abstract->is_abstract($config{pkg_abstract});
		is $_, $SENTINEL, '$_ unchanged after multiple is_abstract() calls';
	}
};

done_testing;



( run in 2.697 seconds using v1.01-cache-2.11-cpan-600a1bdf6e4 )