#!/usr/bin/perl
use lib './perl';
use strict;
use File::Spec;
use File::Basename;
use Cwd 'abs_path';
use JSON qw(from_json to_json);
use LWP;
use HTTP::Request::Common qw(POST GET);
use Data::Dumper;
use Term::ANSIColor 4.00 qw(RESET :constants);
my $scxmlBin = shift;
die ("First argument needs to be path to uscxml-browser binary") if (!$scxmlBin);
die("'" . $scxmlBin . "' is not an executable file") if (! -x $scxmlBin || ! -f $scxmlBin);
my $baseDir = File::Spec->canonpath(dirname($0));
chdir $baseDir;
my $ua = LWP::UserAgent->new;
my $request;
my $response;
my $data;
my @breakpointSeq;
my $pid = fork;
# child process to run the interpreter
if (!$pid) {
open STDOUT, ">", "/dev/null" or die "$0: open: $!";
open STDERR, ">&", \*STDOUT or exit 1;
exec("$scxmlBin -t4088 -d test-http-debugger.scxml");
exit;
}
my $baseURL = 'http://localhost:4088/debug';
# my $baseURL = 'http://localhost:5080/debug';
sleep(1);
sub dumpRequest {
# http://search.cpan.org/~oalders/HTTP-Message-6.13/lib/HTTP/Request.pm
my $request = shift;
return
"\tURI: " . $request->uri() . "\n" .
"\tCONTENT: " . $request->content() . "\n";
}
sub dumpResponse {
# http://search.cpan.org/~oalders/HTTP-Message-6.13/lib/HTTP/Response.pm
my $response = shift;
return
"\tCONTENT: " . $response->content() . "\n";
}
sub post {
my $path = shift;
my $session = shift;
my $data = shift || {};
$data->{'session'} = $session;
my $response;
# read reply until we have something other than a log message from the server
while(1) {
my $request = POST $baseURL.$path, $data;
print RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
# skip log messages
last if ($path ne '/poll');
last if (!exists from_json($response->content())->{'severity'});
}
return $response;
}
sub assertSuccess {
my $response = shift;
my $message = shift;
from_json($response->content())->{'status'} eq "success" or die($message);
}
sub assertFailure {
my $response = shift;
my $message = shift;
from_json($response->content())->{'status'} eq "failure" or die($message);
}
sub attachSession {
my $docName = shift;
$request = POST $baseURL.'/instances';
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not get running sessions");
my $attach = "";
my $sessions = from_json($response->content())->{'instances'};
foreach my $instance (@{$sessions}) {
if ($instance->{'name'} eq $docName) {
$attach = $instance->{'id'};
last;
}
}
$attach or die("Could not attach to instance named $docName\n");
### Get a session
my $request = GET $baseURL.'/connect';
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not connect");
my $session = from_json($response->content())->{'session'};
die("Cannot acquire session from server") if (!$session);
$request = POST $baseURL.'/attach',
[
'attach' => $attach,
'session' => $session,
];
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not get attach to session");
print BOLD BLACK . "Session attached!" . RESET . "\n\n";
return $session;
}
sub prepareSession {
my $source = shift;
### Get a session
my $request = GET $baseURL.'/connect';
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not connect");
my $session = from_json($response->content())->{'session'};
die("Cannot acquire session from server") if (!$session);
$source->{'session'} = $session;
### Prepare an SCXML interpreter
$request = POST $baseURL.'/prepare', $source;
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not prepare SCXML");
print BOLD BLACK . "Session prepared!" . RESET . "\n\n";
return $session;
}
sub finishSession {
my $session = shift;
### Prepare an SCXML interpreter
$request = POST $baseURL.'/disconnect',
[
'session' => $session,
];
print FAINT RED . "-> SEND === line:" . __LINE__ . "\n" . dumpRequest($request) . RESET . "\n";
$response = $ua->request($request);
print FAINT CYAN . "<- RCVD === line:" . __LINE__ . "\n" . dumpResponse($response) . RESET . "\n";
assertSuccess($response, "Could not disconnect session");
print BOLD BLACK . "Session terminated!" . RESET . "\n\n";
}
sub popAndCompare {
my $qualified = shift;
my $bp = shift(@breakpointSeq);
for my $key (keys %{$bp}) {
if (! exists($qualified->{$key}) || $qualified->{$key} ne $bp->{$key}) {
print "found: ".Dumper($qualified);
print "expected: ".Dumper($bp);
die("Expected different breakpoint");
}
}
print BOLD BLACK . "OK!" . RESET . "\n\n";
}
sub testSimpleStepping {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testSimpleStepping " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $xml = << 'END_SCXML';
END_SCXML
@breakpointSeq = (
{ subject => "microstep", when => "before" },
{ subject => "state", when => "before", action => "enter" },
{ subject => "state", when => "after", action => "enter" },
{ subject => "state", when => "before", action => "enter", stateId => "s1" },
{ subject => "executable", when => "before", execName => "log" },
{ subject => "executable", when => "after", execName => "log" },
{ subject => "state", when => "after", action => "enter", stateId => "s1" },
{ subject => "microstep", when => "after" },
{ subject => "microstep", when => "before" },
{ subject => "state", when => "before", action => "exit", stateId => "s1" },
{ subject => "state", when => "after", action => "exit", stateId => "s1" },
{ subject => "transition", when => "before", source => "s1", target => "s2"},
{ subject => "transition", when => "after", source => "s1", target => "s2"},
{ subject => "state", when => "before", action => "enter", stateId => "s2" },
{ subject => "state", when => "after", action => "enter", stateId => "s2" },
{ subject => "microstep", when => "after" },
{ subject => "microstep", when => "before" },
{ subject => "state", when => "before", action => "exit", stateId => "s2" },
{ subject => "state", when => "after", action => "exit", stateId => "s2" },
{ subject => "transition", when => "before", source => "s2", target => "pass"},
{ subject => "transition", when => "after", source => "s2", target => "pass"},
{ subject => "state", when => "before", action => "enter", stateId => "pass" },
{ subject => "state", when => "after", action => "enter", stateId => "pass" },
{ subject => "microstep", when => "after" },
);
my $session = &prepareSession({'xml' => $xml});
print BOLD . "Testing sequence of breakpoints being raised via step". RESET . "\n";
while(@breakpointSeq > 0) {
### Take a step
$response = post('/step', $session);
assertSuccess($response, "Could not step");
### Get the pending messages
$response = post('/poll', $session);
assertSuccess($response, "Could not get breakpoint after step");
# compare to what we expect
$data = from_json($response->content());
popAndCompare($data->{'qualified'});
}
### last step will finalize the interpreter
$response = post('/step', $session);
assertSuccess($response, "Could not step");
### get the pending server push reply
$response = post('/poll', $session);
assertSuccess($response, "Could not get breakpoint after step");
$data = from_json($response->content());
die("Machine not yet finished") if ($data->{'replyType'} ne "finished");
&finishSession($session);
}
sub testBreakpoint {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testBreakpoint " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $xml = << 'END_SCXML';
END_SCXML
my $session = &prepareSession({'xml' => $xml});
print BOLD . "Adding a dedicated breakpoint". RESET . "\n";
$response = post('/breakpoint/add', $session, {
'session' => $session,
'when' => 'after',
'action' => 'enter',
'subject' => 'state',
'stateId' => 's1'
});
assertSuccess($response, "Could not add breakpoint");
print BOLD . "Starting interpretation (will run into breakpoint)". RESET . "\n";
$response = post('/start', $session);
assertSuccess($response, "Could not start interpreter");
print BOLD . "Polling asynchronously for breakpoint hit by interpreter". RESET . "\n";
$response = post('/poll', $session);
assertSuccess($response, "Could not poll for breakpoint");
print BOLD . "Skipping to implicit breakpoint". RESET . "\n";
$response = post('/breakpoint/skipto', $session, {
'session' => $session,
'when' => 'before',
'action' => 'enter',
'subject' => 'state',
'stateId' => 's2'
});
assertSuccess($response, "Could not skip to breakpoint");
print BOLD . "Polling asynchronously for breakpoint hit by interpreter". RESET . "\n";
$response = post('/poll', $session);
assertSuccess($response, "Could not poll for breakpoint");
$data = from_json($response->content());
print Dumper($data);
&finishSession($session);
}
sub testIssueReporting {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testIssueReporting " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $xml = << 'END_SCXML';
END_SCXML
my $session = prepareSession({'xml' => $xml});
print BOLD . "Getting a list of issues with the document". RESET . "\n";
### Get a list of issues
$response = post('/issues', $session);
assertSuccess($response, "Could not get issues for prepared SCXML document");
$data = from_json($response->content());
print Dumper($data);
&finishSession($session);
}
sub testDataModelInspection {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testDataModelInspection " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $session = prepareSession({'url' => 'https://raw.githubusercontent.com/tklab-tud/uscxml/master/test/w3c/ecma/test144.scxml'});
print BOLD . "Skipping to first transition". RESET . "\n";
$response = post('/breakpoint/skipto', $session, {
'when' => 'before',
'subject' => 'transition',
'target' => 's1'
});
assertSuccess($response, "Could not add breakpoint");
print BOLD . "Polling asynchronously for breakpoint hit by interpreter". RESET . "\n";
$response = post('/poll', $session);
assertSuccess($response, "Could not get breakpoint after step");
print BOLD . "Evaluating expression '_event' on the datamodel". RESET . "\n";
$response = post('/eval', $session, {
'expression' => '_event'
});
assertSuccess($response, "Could not evaluate expression");
print BOLD . "Evaluating expression '_event' on the datamodel". RESET . "\n";
$response = post('/eval', $session, {
'expression' => '_ioprocessors'
});
assertSuccess($response, "Could not evaluate expression");
&finishSession($session);
}
sub testSessionAttaching {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testSessionAttaching " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $session = attachSession("test-http-debugger.scxml");
print BOLD . "Skipping to first transition". RESET . "\n";
$response = post('/breakpoint/skipto', $session, {
'when' => 'before',
'subject' => 'transition',
'target' => 's1'
});
assertSuccess($response, "Could not add breakpoint");
print BOLD . "Polling asynchronously for breakpoint hit by interpreter". RESET . "\n";
$response = post('/poll', $session);
assertSuccess($response, "Could not get breakpoint after step");
&finishSession($session);
}
sub testEventInsertion {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testEventInsertion " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $xml = << 'END_SCXML';
END_SCXML
my $session = prepareSession({'xml' => $xml});
print BOLD . "Sending event" . RESET . "\n";
$response = post('/event', $session, {
'name' => 'foo',
});
assertSuccess($response, "Could not send event");
&finishSession($session);
}
sub testRunningStates {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testEventInsertion " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $session = attachSession("test-http-debugger.scxml");
print BOLD . "Stopping the intepreter". RESET . "\n";
$response = post('/stop', $session);
assertFailure($response, "Should not be able to stop attached interpreter");
print BOLD . "Starting the intepreter". RESET . "\n";
$response = post('/start', $session);
assertFailure($response, "Should not be able to start attached interpreter");
print BOLD . "Pausing the intepreter". RESET . "\n";
$response = post('/pause', $session);
assertSuccess($response, "Could not pause the attached interpreter");
print BOLD . "Polling state of paused interpreter". RESET . "\n";
$response = post('/poll', $session);
assertSuccess($response, "Could not poll state of paused interpreter");
print BOLD . "Pausing the intepreter again". RESET . "\n";
$response = post('/pause', $session);
assertFailure($response, "Should not be able to pause attached interpreter twice");
print BOLD . "Resuming the intepreter". RESET . "\n";
$response = post('/resume', $session);
assertSuccess($response, "Could not resume the attached interpreter");
print BOLD . "Resuming the intepreter again". RESET . "\n";
$response = post('/resume', $session);
assertFailure($response, "Should not be able to resume attached interpreter twice");
&finishSession($session);
}
sub testLogReception {
print BOLD WHITE ON_RED . " " . RESET ."\n";
print BOLD WHITE ON_RED . " testLogReception " . RESET ."\n";
print BOLD WHITE ON_RED . " " . RESET ."\n\n";
my $session = prepareSession({'url' => abs_path($baseDir).'/test-http-debugger.scxml'});
print BOLD . "Starting the intepreter". RESET . "\n";
$response = post('/start', $session);
assertSuccess($response, "Could not start interpreter");
for (my $i = 0; $i < 5; $i++) {
print BOLD . "Polling interpreter for messages". RESET . "\n";
# the trailing /# is only for poll above not to disregard the message
# You would just use /poll to get any pending message from the server
$response = post('/poll/#', $session);
assertSuccess($response, "Could not poll for messages");
}
&finishSession($session);
}
&testSimpleStepping();
&testBreakpoint();
&testIssueReporting();
&testDataModelInspection();
&testSessionAttaching();
&testEventInsertion();
&testRunningStates();
&testLogReception();
kill('TERM', $pid);