Config-Abstraction
view release on metacpan or search on metacpan
t/function.t view on Meta::CPAN
subtest '_load_driver() - returns false for nonexistent module' => sub {
my $cfg = _make_cfg();
my $result = $cfg->_load_driver('No::Such::Module::XYZ');
ok(!$result, '_load_driver returns false for missing module');
};
subtest '_load_driver() - caches failed load' => sub {
my $cfg = _make_cfg();
$cfg->_load_driver('No::Such::Module::XYZ');
ok($cfg->{failed}{'No::Such::Module::XYZ'}, 'failed load cached in {failed}');
};
subtest '_load_driver() - skips reload of already-loaded module' => sub {
my $cfg = _make_cfg();
$cfg->{loaded}{'Scalar::Util'} = 1;
my $result = $cfg->_load_driver('Scalar::Util');
is($result, 1, 'returns 1 from cache without re-requiring');
};
subtest '_load_driver() - skips retry of already-failed module' => sub {
my $cfg = _make_cfg();
$cfg->{failed}{'No::Such::Module::XYZ'} = 1;
my $result = $cfg->_load_driver('No::Such::Module::XYZ');
ok(!$result, 'returns false from cache without re-attempting');
};
# ===========================================================================
# Environment variable merging (via _load_config internals)
# ===========================================================================
subtest 'ENV vars with prefix override data values' => sub {
local %ENV = %ENV;
$ENV{'TESTAPP_RETRIES'} = '99';
my $cfg = Config::Abstraction->new(
data => { TESTAPP => { retries => $EXPECTED_RETRIES } },
config_dirs => [],
env_prefix => $ENV_PREFIX,
);
is($cfg->get('TESTAPP.retries'), '99', 'ENV var stored under prefix namespace');
};
subtest 'ENV vars with double-underscore create nested keys' => sub {
local %ENV = %ENV;
$ENV{'TESTAPP_DATABASE__USER'} = 'env_user';
my $cfg = Config::Abstraction->new(
data => {
database => { user => $EXPECTED_USER, pass => $EXPECTED_PASS },
retries => $EXPECTED_RETRIES,
},
config_dirs => [],
env_prefix => $ENV_PREFIX,
);
is($cfg->get('database.user'), 'env_user', 'double-underscore ENV creates nested key');
};
# ===========================================================================
# Command-line argument merging (via _load_config internals)
# ===========================================================================
subtest 'CLI args override data values' => sub {
local @ARGV = ("--TESTAPP_RETRIES=77");
my $cfg = Config::Abstraction->new(
data => { retries => $EXPECTED_RETRIES },
config_dirs => [],
env_prefix => $ENV_PREFIX,
);
is($cfg->get('retries'), '77', 'CLI arg overrides data value');
};
subtest 'CLI args with double-underscore create nested keys' => sub {
# \%NESTED_DATA must not be used here - the CLI merge path modifies nested
# hashrefs in-place via shared references from the shallow copy of 'data',
# which would attempt to modify the Readonly nested hashrefs and die.
# Use a fresh anonymous hash instead so the merge can write freely.
local @ARGV = ('--TESTAPP_DATABASE__USER=cli_user');
my $cfg = Config::Abstraction->new(
data => {
database => { user => $EXPECTED_USER, pass => $EXPECTED_PASS },
retries => $EXPECTED_RETRIES,
},
config_dirs => [],
env_prefix => $ENV_PREFIX,
);
is($cfg->get('database.user'), 'cli_user', 'CLI double-underscore creates nested key');
};
# ===========================================================================
# Coderef / blessed-object protection (regression for corruption bug)
# ===========================================================================
subtest 'coderef in data not corrupted by _load_config' => sub {
my $cb = sub { $EXPECTED_CB_RESULT };
my $cfg = Config::Abstraction->new(
data => { callback => $cb, tags => 'alpha,beta' },
config_dirs => [],
);
my $got = $cfg->get('callback');
is(reftype($got), 'CODE', 'coderef type intact after load');
is($got->(), $EXPECTED_CB_RESULT, 'coderef callable after load');
};
subtest 'blessed object in data not corrupted by _load_config' => sub {
my $obj = bless { v => $EXPECTED_PORT }, '_BlessedVal';
my $cfg = Config::Abstraction->new(
data => { handler => $obj },
config_dirs => [],
);
my $got = $cfg->get('handler');
ok(blessed($got), 'blessed object intact after load');
is(blessed($got), '_BlessedVal', 'class name unchanged after load');
};
# ===========================================================================
# TestProxy -- minimal subclass that satisfies the UNIVERSAL::isa guard on
# _load_remote_dir and _parse_config_string. Calling those methods directly
# from 'main' would croak; routing through a genuine subclass keeps the guard
# happy without altering the behaviour under test.
# ===========================================================================
package Config::Abstraction::TestProxy;
use parent -norequire, 'Config::Abstraction';
sub test_load_remote_dir { my $self = shift; return $self->_load_remote_dir(@_) }
sub test_parse_config_string { my $self = shift; return $self->_parse_config_string(@_) }
package main;
# Provide a minimal stub so remote-path tests run whether or not
# File::Slurp::Remote is installed. Individual subtests mock the body.
unless(eval { require File::Slurp::Remote; 1 }) {
no warnings 'once';
*File::Slurp::Remote::read_file = sub {};
$INC{'File/Slurp/Remote.pm'} = 1;
}
Readonly::Scalar my $REMOTE_HOST => 'cfg.example.com';
Readonly::Scalar my $REMOTE_DIR => '/etc/myapp';
Readonly::Scalar my $REMOTE_USER => 'deploy';
t/function.t view on Meta::CPAN
ok(exists $es->{'retries'}, 'top-level key has audit record');
is(ref($es->{'retries'}{sources}), 'ARRAY', 'sources field is an arrayref');
};
subtest 'explain_sources() - data source recorded with correct type and label' => sub {
my $cfg = _make_cfg();
my $es = $cfg->explain_sources();
my @data_srcs = grep { $_->{type} eq 'data' } @{$es->{'retries'}{sources}};
ok(scalar(@data_srcs) > 0, 'at least one data-layer source found');
is($data_srcs[0]{label}, 'constructor data argument',
'data source label is "constructor data argument"');
is($data_srcs[0]{value}, $EXPECTED_RETRIES, 'data source value correct');
};
subtest 'explain_sources() - final value matches get() for same key' => sub {
my $cfg = _make_cfg();
my $es = $cfg->explain_sources();
is($es->{'retries'}{value}, $cfg->get('retries'),
'explain_sources final value equals get() result');
};
subtest 'explain_sources() - env source recorded with env var name as label' => sub {
local %ENV = %ENV;
delete $ENV{APP_DATABASE__HOST};
$ENV{APP_DATABASE__HOST} = 'env_host_value';
my $cfg = Config::Abstraction->new(
data => { database => { host => 'default' } },
config_dirs => [],
env_prefix => 'APP_',
);
my $es = $cfg->explain_sources();
my @env_srcs = grep { $_->{type} eq 'env' } @{$es->{'database.host'}{sources}};
ok(scalar(@env_srcs) > 0, 'env source recorded');
is($env_srcs[0]{label}, 'APP_DATABASE__HOST', 'env label is the env var name');
is($env_srcs[0]{value}, 'env_host_value', 'env source value correct');
diag("env source: " . $env_srcs[0]{label}) if $ENV{TEST_VERBOSE};
};
# ===========================================================================
# _value_from_type() -- source record lookup internals
# ===========================================================================
subtest '_value_from_type() - (0, undef) for undef key' => sub {
my $cfg = _make_cfg();
my ($found, $val) = $cfg->_value_from_type('data', undef);
is($found, 0, 'found=0 for undef key');
ok(!defined($val), 'val=undef for undef key');
};
subtest '_value_from_type() - (1, value) for data-sourced key' => sub {
my $cfg = _make_cfg();
my ($found, $val) = $cfg->_value_from_type('data', 'retries');
is($found, 1, 'found=1');
is($val, $EXPECTED_RETRIES, 'correct value');
};
subtest '_value_from_type() - (0, undef) when type did not contribute' => sub {
# No CLI args in @ARGV, so argv layer should not have contributed
local @ARGV = ();
my $cfg = _make_cfg();
my ($found, $val) = $cfg->_value_from_type('argv', 'retries');
ok(!$found, 'found is false when argv did not set the key');
ok(!defined($val), 'val=undef when argv did not contribute');
};
subtest '_value_from_type() - normalises sep_char to dot before lookup' => sub {
# Source records always use '.' regardless of sep_char.
# _value_from_type must translate sep_char-separated keys before searching.
my $cfg = Config::Abstraction->new(
data => { database => { user => $EXPECTED_USER } },
config_dirs => [],
sep_char => '/',
);
my ($found, $val) = $cfg->_value_from_type('data', 'database/user');
is($found, 1, 'sep_char normalised to dot for record lookup');
is($val, $EXPECTED_USER, 'correct value after normalisation');
};
# ===========================================================================
# prefer_env() / prefer_file() / prefer_data() / prefer_argv()
# ===========================================================================
subtest 'prefer_data() - returns data-layer value even when file overrides it' => sub {
plan skip_all => 'requires filesystem' if $^O eq 'MSWin32';
require File::Temp;
my $dir = File::Temp::tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/base.yaml" or die "Cannot write: $!";
print $fh "retries: 99\n";
close $fh;
my $cfg = Config::Abstraction->new(
data => { retries => $EXPECTED_RETRIES },
config_dirs => [$dir],
);
# file sets retries=99; prefer_data must bypass the file and return the data value
is($cfg->prefer_data('retries'), $EXPECTED_RETRIES,
'prefer_data returns data value (3) despite file override (99)');
is($cfg->get('retries'), 99, 'get() confirms file override wins in merged config');
};
subtest 'prefer_data() - falls back to get() when data layer did not set the key' => sub {
plan skip_all => 'requires filesystem' if $^O eq 'MSWin32';
require File::Temp;
my $dir = File::Temp::tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/base.yaml" or die;
print $fh "only_in_file: yes\n";
close $fh;
my $cfg = Config::Abstraction->new(
data => { something_else => 1 },
config_dirs => [$dir],
);
is($cfg->prefer_data('only_in_file'), 'yes',
'prefer_data falls back to get() for key not in data layer');
};
subtest 'prefer_env() - returns env value, not higher-priority argv override' => sub {
local %ENV = %ENV;
delete $ENV{APP_DATABASE__HOST};
$ENV{APP_DATABASE__HOST} = 'env-host';
local @ARGV = ('--APP_DATABASE__HOST=argv-host');
my $cfg = Config::Abstraction->new(
data => { database => { host => 'default' } },
config_dirs => [],
env_prefix => 'APP_',
);
is($cfg->prefer_env('database.host'), 'env-host',
'prefer_env returns env value, bypassing argv');
is($cfg->get('database.host'), 'argv-host',
'get() confirms argv wins in merged config');
};
subtest 'prefer_env() - falls back to get() when no env contributed' => sub {
local %ENV = %ENV;
delete $ENV{APP_RETRIES};
my $cfg = _make_cfg();
is($cfg->prefer_env('retries'), $EXPECTED_RETRIES,
'prefer_env falls back to get() when env did not contribute');
};
subtest 'prefer_argv() - returns argv value when CLI arg provided' => sub {
local @ARGV = ('--APP_DATABASE__HOST=argv-host');
local %ENV = %ENV;
delete $ENV{APP_DATABASE__HOST};
my $cfg = Config::Abstraction->new(
data => { database => { host => 'default' } },
config_dirs => [],
env_prefix => 'APP_',
);
is($cfg->prefer_argv('database.host'), 'argv-host',
'prefer_argv returns argv-layer value');
};
subtest 'prefer_argv() - falls back to get() when no argv contributed' => sub {
local @ARGV = ();
my $cfg = _make_cfg();
is($cfg->prefer_argv('retries'), $EXPECTED_RETRIES,
'prefer_argv falls back to get() when @ARGV did not contribute');
};
subtest 'prefer_file() - returns file value when a file set the key' => sub {
plan skip_all => 'requires filesystem' if $^O eq 'MSWin32';
require File::Temp;
my $dir = File::Temp::tempdir(CLEANUP => 1);
open my $fh, '>', "$dir/base.yaml" or die;
print $fh "retries: 99\n";
close $fh;
local %ENV = %ENV;
delete $ENV{APP_RETRIES};
my $cfg = Config::Abstraction->new(
data => { retries => $EXPECTED_RETRIES },
config_dirs => [$dir],
);
is($cfg->prefer_file('retries'), 99,
'prefer_file returns file-layer value (99)');
};
subtest 'prefer_file() - falls back to get() when no file set the key' => sub {
# data-only config: no file layer contributed
my $cfg = _make_cfg();
is($cfg->prefer_file('retries'), $EXPECTED_RETRIES,
'prefer_file falls back to get() when no file contributed');
};
# ===========================================================================
# _flatten_keys() -- nested hash flattening (package function, no $self)
# ===========================================================================
subtest '_flatten_keys() - three-level nesting produces dotted key' => sub {
my %flat = Config::Abstraction::_flatten_keys({ a => { b => { c => 'leaf' } } });
is($flat{'a.b.c'}, 'leaf', 'three-level nesting produces a.b.c');
is(scalar(keys %flat), 1, 'exactly one key');
};
subtest '_flatten_keys() - top-level scalars passed through' => sub {
my %flat = Config::Abstraction::_flatten_keys({ x => 1, y => 'str' });
is($flat{x}, 1, 'integer preserved');
is($flat{y}, 'str', 'string preserved');
};
subtest '_flatten_keys() - config_path key skipped at every level' => sub {
my %flat = Config::Abstraction::_flatten_keys({
normal => 'v',
config_path => ['/path'],
nested => { config_path => ['/n'] },
});
ok( exists $flat{normal}, 'normal key present');
ok(!exists $flat{config_path}, 'top-level config_path skipped');
};
subtest '_flatten_keys() - empty hash gives empty flat hash' => sub {
my %flat = Config::Abstraction::_flatten_keys({});
is(scalar(keys %flat), 0, 'empty input -> empty output');
};
( run in 1.129 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )