Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Template/Parser.pm
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ sub interpolate_text {
}
# $variable reference
if ($var) {
$line += $dir =~ tr/\n/ /;
$line += $dir =~ tr/\n//;
push(@tokens, [ $dir, $line, $self->tokenise_directive($var) ]);
}
# other '$' reference - treated as text
Expand Down
17 changes: 9 additions & 8 deletions lib/Template/Provider.pm
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,8 @@ sub _init {
$path = [ split(/$dlim/, $path) ]
unless ref $path eq 'ARRAY';

# don't allow a CACHE_SIZE 1 because it breaks things and the
# additional checking isn't worth it
$size = 2
if defined $size && ($size == 1 || $size < 0);
$size = 0
if defined $size && $size < 0;

if (defined ($debug = $params->{ DEBUG })) {
$self->{ DEBUG } = $debug & ( Template::Constants::DEBUG_PROVIDER
Expand Down Expand Up @@ -793,16 +791,19 @@ sub _store {

# remove entry from tail of list
$slot = $self->{ TAIL };
$slot->[ PREV ]->[ NEXT ] = undef;
$self->{ TAIL } = $slot->[ PREV ];
if ($slot->[ PREV ]) {
$slot->[ PREV ]->[ NEXT ] = undef;
$self->{ TAIL } = $slot->[ PREV ];
}

# remove name lookup for old node
delete $self->{ LOOKUP }->{ $slot->[ NAME ] };

# add modified node to head of list
$head = $self->{ HEAD };
$head->[ PREV ] = $slot if $head;
@$slot = ( undef, $name, $data, $load, $head, time );
$head->[ PREV ] = $slot if $head && $head != $slot;
@$slot = ( undef, $name, $data, $load,
($head && $head != $slot ? $head : undef), time );
$self->{ HEAD } = $slot;

# add name lookup for new node
Expand Down
57 changes: 57 additions & 0 deletions t/interp_newline.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#============================================================= -*-perl-*-
#
# t/interp_newline.t
#
# Test that interpolate_text() preserves newlines in ${variable}
# references and tracks line numbers correctly.
#
#========================================================================

use strict;
use warnings;
use lib qw( ./lib ../lib );
use Test::More;
use Template::Parser;

my $parser = Template::Parser->new({});

# Single-line variable: $dir should be '${ foo }'
{
my $tokens = $parser->interpolate_text('before ${ foo } after', 1);
my @directives = grep { ref $_ } @$tokens;
is(scalar @directives, 1, 'one directive in single-line interp');
is($directives[0][0], '${ foo }', 'directive text preserved');
}

# Multi-line variable: newlines in ${...} must NOT be replaced with spaces
{
my $text = "before \${\nfoo\n} after";
my $tokens = $parser->interpolate_text($text, 1);
my @directives = grep { ref $_ } @$tokens;
is(scalar @directives, 1, 'one directive in multi-line interp');
like($directives[0][0], qr/\n/, 'newlines preserved in directive text');
is($directives[0][0], "\${\nfoo\n}", 'exact directive text with newlines');
is($directives[0][1], 3, 'line number advanced past newlines');
}

# Verify line counting is correct with multiple variables
{
my $text = "line1\nline2 \${ a } line2\nline3 \${ b }";
my $tokens = $parser->interpolate_text($text, 1);
my @directives = grep { ref $_ } @$tokens;
is(scalar @directives, 2, 'two directives found');
is($directives[0][1], 2, 'first var on line 2');
is($directives[1][1], 3, 'second var on line 3');
}

# End-to-end: INTERPOLATE mode renders multiline ${} correctly
{
require Template;
my $tt = Template->new({ INTERPOLATE => 1 });
my $output = '';
my $vars = { foo => 'HELLO' };
$tt->process(\"start \${\nfoo\n} end", $vars, \$output) or die $tt->error;
is($output, 'start HELLO end', 'multiline ${} interpolation works');
}

done_testing();
167 changes: 167 additions & 0 deletions t/provider_cache.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
#============================================================= -*-perl-*-
#
# t/provider_cache.t
#
# Test Template::Provider cache behavior, including edge cases
# for small CACHE_SIZE values.
#
#========================================================================

use strict;
use warnings;
use lib qw( ./lib ../lib );
use File::Temp qw(tempdir);
use File::Spec;
use Test::More;
use Template::Provider;

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

sub write_template {
my ($name, $content) = @_;
my $path = File::Spec->catfile($dir, $name);
open my $fh, '>', $path or die "Cannot write $path: $!";
print $fh $content;
close $fh;
return $path;
}

write_template('alpha.tt', 'Alpha content');
write_template('beta.tt', 'Beta content');
write_template('gamma.tt', 'Gamma content');

#-----------------------------------------------------------------------
# CACHE_SIZE = 1: the cache holds exactly one compiled template
#-----------------------------------------------------------------------

subtest 'CACHE_SIZE 1 — basic load and eviction' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 1,
});

my ($data, $error) = $provider->fetch('alpha.tt');
ok($data, 'loaded alpha.tt with CACHE_SIZE=1');
is($error, undef, 'no error on first load');

($data, $error) = $provider->fetch('beta.tt');
ok($data, 'loaded beta.tt — evicted alpha.tt');
is($error, undef, 'no error on second load');

($data, $error) = $provider->fetch('gamma.tt');
ok($data, 'loaded gamma.tt — evicted beta.tt');
is($error, undef, 'no error on third load');
};

subtest 'CACHE_SIZE 1 — reload same template' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 1,
});

my ($data1, $err1) = $provider->fetch('alpha.tt');
ok($data1, 'first load of alpha.tt');

my ($data2, $err2) = $provider->fetch('alpha.tt');
ok($data2, 'second load of alpha.tt (cache hit)');
is($err2, undef, 'no error on cache hit');
};

subtest 'CACHE_SIZE 1 — alternate between two templates' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 1,
});

for my $i (1..5) {
my $name = $i % 2 ? 'alpha.tt' : 'beta.tt';
my ($data, $error) = $provider->fetch($name);
ok($data, "iteration $i: loaded $name");
is($error, undef, "iteration $i: no error");
}
};

#-----------------------------------------------------------------------
# CACHE_SIZE = 2: verify eviction doesn't corrupt the linked list
#-----------------------------------------------------------------------

subtest 'CACHE_SIZE 2 — three templates trigger eviction' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 2,
});

my ($d, $e);
($d, $e) = $provider->fetch('alpha.tt');
ok($d, 'alpha.tt loaded (slot 1/2)');

($d, $e) = $provider->fetch('beta.tt');
ok($d, 'beta.tt loaded (slot 2/2)');

($d, $e) = $provider->fetch('gamma.tt');
ok($d, 'gamma.tt loaded — evicted alpha.tt');
is($e, undef, 'no error after eviction');

($d, $e) = $provider->fetch('alpha.tt');
ok($d, 'alpha.tt reloaded after eviction');
};

#-----------------------------------------------------------------------
# CACHE_SIZE = 0: caching disabled
#-----------------------------------------------------------------------

subtest 'CACHE_SIZE 0 — caching disabled' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 0,
});

my ($data, $error) = $provider->fetch('alpha.tt');
ok($data, 'loaded alpha.tt with caching disabled');
is($error, undef, 'no error');

($data, $error) = $provider->fetch('beta.tt');
ok($data, 'loaded beta.tt with caching disabled');
is($error, undef, 'no error');
};

#-----------------------------------------------------------------------
# Negative CACHE_SIZE: treated as disabled (0)
#-----------------------------------------------------------------------

subtest 'negative CACHE_SIZE — treated as disabled' => sub {
my $provider = Template::Provider->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => -1,
});

my ($data, $error) = $provider->fetch('alpha.tt');
ok($data, 'loaded with negative CACHE_SIZE');
is($error, undef, 'no error');
};

#-----------------------------------------------------------------------
# Template content verification through Template.pm
#-----------------------------------------------------------------------

subtest 'CACHE_SIZE 1 — correct content returned' => sub {
require Template;
my $tt = Template->new({
INCLUDE_PATH => $dir,
CACHE_SIZE => 1,
});

my $output = '';
$tt->process('alpha.tt', {}, \$output) or die $tt->error;
is($output, 'Alpha content', 'alpha.tt content correct');

$output = '';
$tt->process('beta.tt', {}, \$output) or die $tt->error;
is($output, 'Beta content', 'beta.tt content correct after eviction');

$output = '';
$tt->process('alpha.tt', {}, \$output) or die $tt->error;
is($output, 'Alpha content', 'alpha.tt content correct after re-eviction');
};

done_testing();
Loading