webgui/t/Asset/Wobject/Survey/ExpressionEngine.t
Patrick Donelan 804dbcfd26 One-line fix for Survey ExpressionEngine bug that took over a day to find.
One of the file-scoped lexicals in ExpressionEngine wasn't being initialised,
which meant that tagged data was being cached across repeated engine runs
(including, would you believe, across modperl page requests).

The fix includes tests.
2009-11-05 21:15:47 -08:00

274 lines
12 KiB
Perl

# Tests WebGUI::Asset::Wobject::Survey
#
#
use strict;
use warnings;
use FindBin;
use lib "$FindBin::Bin/../../../lib";
use Test::More;
use Test::Deep;
use Test::MockObject::Extends;
use Test::Exception;
use Data::Dumper;
use List::Util qw/shuffle/;
use WebGUI::Test; # Must use this before any other WebGUI modules
use WebGUI::Session;
use Tie::IxHash;
#----------------------------------------------------------------------------
# Init
my $session = WebGUI::Test->session;
#----------------------------------------------------------------------------
# Tests
my $tests = 60;
plan tests => $tests + 1;
#----------------------------------------------------------------------------
# put your tests here
my $usedOk = use_ok('WebGUI::Asset::Wobject::Survey::ExpressionEngine');
my ($user, $survey, $versionTag);
SKIP: {
skip $tests, "Unable to load ExpressionEngine" unless $usedOk;
my $e = "WebGUI::Asset::Wobject::Survey::ExpressionEngine";
WebGUI::Test->originalConfig('enableSurveyExpressionEngine');
$session->config->set( 'enableSurveyExpressionEngine', 0 );
is( $e->run( $session, 'jump { 1 } target' ),
undef, "Nothing happens unless we turn on enableSurveyExpressionEngine in config" );
$session->config->set( 'enableSurveyExpressionEngine', 1 );
cmp_deeply( $e->run( $session, 'jump { 1 } target' ), { jump => 'target', tags => {} }, "..now we're in business!" );
my %values = (
n => 5,
s1 => 'my string',
multi => [ 'answer1', 'answer2' ],
);
my %scores = (
n1 => 1,
n2 => 2,
);
# These should all jump to 'target'
my @should_jump = (
q{jump { 1 } target},
q{jump { return 1 } target},
q{jump { "string" } target},
q{jump { value(n) == 5 } target},
q{jump { value(n) > 0 } target},
q{jump { value(s1) eq "my string" } target},
q{jump { value(s1) =~ m/my/ } target},
q{jump { value(n) == 4 or value(n) == 5 } target},
q{jump { value(n) == 5 && value(n) > 0 } target},
q{jump { (value(n) > 1 ? 10 : 11) == 10 } target},
q{jump { $a=1; $a++; $a++; $a *= 2; $a == 6 } target},
q{jump { @a = (1..10); $a[0] == 1 && @a == 10 } target}, # arrays
q{jump { if (value(n) == 5) { 1 } else { 0 } } target}, # if statement
q{jump { $q2 = 3; $avg = (value(n) + $q2) / 2; $avg == 4 } target}, # look ma, averages!
q{jump { $q2 = 3; avg(value(n), $q2) == 4 } target}, # look ma, built-in avg sub!
q{jump { round(3.456) == 3 && round(3.456, 2) == 3.46 } target}, # rounding
q{jump { value(n) == 5 } target; jump { value(n) == 5 } targetX}, # first jump wins
q{jump { value(n) == 0 } targetX; jump { value(n) == 5 } target}, # false jumps ignored
q{jump { min(3,5,2) == 2 } target}, # List::Util min
q{jump { sum(value(n),1,1,1) == 8 } target}, # List::Util sum, etc..
q{jump { score(n1) == 1 && score(n2) == 2 } target}, # score() works
q{jump { answered(n) && !answered(X) } target}, # answered() works
q{jump { value(multi) eq 'answer1, answer2' } target}, # multi-answer question stringifies in scalar context
q{jump { (value(multi))[1] eq 'answer2' } target}, # multi-answer question returns list in list context
q{ sub mySub { return $_[0] + 2 } jump { mySub(1) == 3 } target }, # expressions can define and use subs
q{ jump { (sort { $a <=> $b } ( 5, 4, 3, 2 ))[1] == 3 } target }, # sorting allowed
);
my @should_not_jump = (
q{}, # empty
q{ return }, # empty
q{1}, # doesn't call jump
q{blah-dee-blah-blah}, # rubbish expression
q{jump {} target}, # empty anon sub to jump
q{jump { 0 } target}, # false sub to jump
q{jump { value(n) == 500 } target},
q{jump { value(s1) eq 'blah' } target},
);
my @should_fail = (
q|{|, # doesn't compile
q{jump { time } target}, # time and other opcodes not allowed
);
# These ones should have 'target' as the jump target
for my $expr (@should_jump) {
cmp_deeply( $e->run( $session, $expr, { values => \%values, scores => \%scores, tags => {} } ),
{ jump => 'target', tags => {} }, "\"$expr\" jumps as expected" );
}
# These ones should come back with an undefined jump target
for my $expr (@should_not_jump) {
cmp_deeply( $e->run( $session, $expr, { values => \%values, scores => \%scores, tags => {} } ),
{ jump => undef, tags => {} }, "\"$expr\" does not jump" );
}
# These ones should return undef (general failure to run)
for my $expr (@should_fail) {
is( $e->run( $session, $expr, { values => \%values, scores => \%scores } ),
undef,, "\"$expr\" fails as expected" );
}
$e->run( $session, q{jump {$x = value(s1); $x = 'X'} target}, { values => \%values } );
is( $values{s1}, 'my string', "Expression can't modify values" );
like( $e->run( $session, '{', { validate => 1 } ), qr/Missing right curly/, "Validation option works" );
# Check validTargets option
cmp_deeply( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { a => 1 } } ),
{ jump => undef, tags => {} }, 'target is not valid' );
cmp_deeply( $e->run( $session, q{jump {1} target}, { values => \%values, validTargets => { target => 1 } } ),
{ jump => 'target', tags => {} }, '..whereas now it is ok' );
# Try some tagging
cmp_deeply(
$e->run( $session, q{}, { values => \%values } ),
{ jump => undef, tags => {} },
'returns empty hash for tags by default'
);
cmp_deeply(
$e->run( $session, q{}, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 1 } },
'existing tag values survive'
);
cmp_deeply(
$e->run( $session, q{ tag(a,2) }, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 2 } },
'..but can be changed'
);
cmp_deeply(
$e->run( $session, q{ tag(b) }, { values => \%values, tags => { a => 1 } } ),
{ jump => undef, tags => { a => 1, b => 1 } },
'..and new values can be set (defaults to 1)'
);
cmp_deeply(
$e->run( $session, q{ jump{ tagged(a) } target }, { values => \%values, tags => { a => 1 } } ),
{ jump => 'target', tags => { a => 1 } },
'..flag can be checked with tagged()'
);
cmp_deeply(
$e->run( $session, q{ jump{ tagged(a) eq 'abc' } target }, { values => \%values, tags => { a => 'abc' } } ),
{ jump => 'target', tags => { a => 'abc' } },
'..and any sort of tagged data returned'
);
cmp_deeply(
$e->run( $session, q{ tag(a,xyz); jump{ tagged(a) eq 'xyz' } target }, { values => {a => 'def'}, tags => { a => 'abc' } } ),
{ jump => 'target', tags => { a => 'xyz' } },
'..overwritten tag data can be used too'
);
# Try the exitUrl sub
cmp_deeply(
$e->run( $session, q{ exitUrl(blah)} ),
{ exitUrl => 'blah', tags => { } },
'explicit exitUrl works'
);
cmp_deeply(
$e->run( $session, q{ exitUrl()} ),
{ exitUrl => undef, tags => { } },
'..as does unspecified exitUrl'
);
# Try the restart sub
cmp_deeply(
$e->run( $session, q{ restart} ),
{ restart => 1, tags => { } },
'restart works'
);
# Create a test user
$user = WebGUI::User->new( $session, 'new' );
WebGUI::Test->usersToDelete($user);
# Create a Survey
$versionTag = WebGUI::VersionTag->getWorking($session);
$survey = WebGUI::Asset->getImportNode($session)->addChild(
{ className => 'WebGUI::Asset::Wobject::Survey',
},
);
isa_ok($survey, 'WebGUI::Asset::Wobject::Survey');
my $url = $survey->get('url');
my $id = $survey->getId;
$survey->surveyJSON->newObject([]); # s0
$survey->surveyJSON->newObject([0]); # s0q0
$survey->surveyJSON->newObject([0,0]); # s0q0a0
$survey->surveyJSON->newObject([0]); # s0q1
$survey->surveyJSON->newObject([0,1]); # s0q1a0
$survey->surveyJSON->section([0])->{variable} = 'ext_s0';
$survey->surveyJSON->question([0,0])->{variable} = 'ext_s0q0';
$survey->surveyJSON->question([0,1])->{variable} = 'ext_s0q1';
$survey->surveyJSON->answer([0,0,0])->{recordedAnswer} = 'ext_s0q0a0';
$survey->surveyJSON->answer([0,0,0])->{value} = 150; # worth 150 points
$survey->surveyJSON->answer([0,1,0])->{recordedAnswer} = 'ext_s0q1a0';
$survey->surveyJSON->answer([0,1,0])->{value} = 50; # worth 50 points
my $responseId = $survey->responseId( { userId => $user->userId } );
my $rJSON = $survey->responseJSON(undef, $responseId);
$rJSON->recordResponses({
'0-0-0' => 'My ext_s0q0a0 answer',
'0-1-0' => 'My ext_s0q1a0 answer',
});
$rJSON->processExpression(q{ tag(ext_tag, 199) });
# Remember to persist our changes..
$survey->persistSurveyJSON();
$survey->persistResponseJSON();
$survey->surveyEnd;
cmp_deeply( $e->run( $session, qq{jump {valueX('$id', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external value resolves ok when id used' );
cmp_deeply( $e->run( $session, qq{jump {valueX('$url', ext_s0q0) eq 'ext_s0q0a0'} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external value resolves ok when url used' );
cmp_deeply( $e->run( $session, qq{jump {scoreX('$url', ext_s0q0) == 150} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external score resolves ok too' );
cmp_deeply( $e->run( $session, qq{jump {scoreX('$url', ext_s0) == 200} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external score section totals work too' );
cmp_deeply( $e->run( $session, qq{jump {taggedX('$url', ext_tag) == 199} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'external tag lookups work too' );
# Test for nasty bugs caused by file-scoped lexicals not being properly initialised in L<ExpressionEngine::run>
{
# Create a second test user
my $survey2 = WebGUI::Asset::Wobject::Survey->new($session, $survey->getId);
my $user2 = WebGUI::User->new( $session, 'new' );
WebGUI::Test->usersToDelete($user2);
$session->user({userId => $user2->userId});
my $responseId2 = $survey2->responseId( { userId => $user2->userId } );
my $rJSON2 = $survey2->responseJSON(undef, $responseId2);
$rJSON2->recordResponses({
'0-0-0' => 'My ext_s0q0a0 answer',
'0-1-0' => 'My ext_s0q1a0 answer',
});
$rJSON2->processExpression(q{ tag(ext_tag, 299) });
# Remember to persist our changes..
$survey2->persistSurveyJSON();
$survey2->persistResponseJSON();
$survey2->surveyEnd;
cmp_deeply( $e->run( $session, qq{jump {taggedX('$url', ext_tag) == 299} target}, {userId => $user2->userId} ),
{ jump => 'target', tags => {} }, 'external tag not cached' );
cmp_deeply( $e->run( $session, qq{jump {taggedX('$url', ext_tag) == 199} target}, {userId => $user->userId} ),
{ jump => 'target', tags => {} }, 'first external tag lookups still works' );
}
}
#----------------------------------------------------------------------------
# Cleanup
END {
$survey->purge if $survey;
$versionTag->rollback if $versionTag;
}