File Coverage

inc/TestML/Runtime.pm
Criterion Covered Total %
statement 190 262 72.5
branch 41 104 39.4
condition 10 21 47.6
subroutine 39 63 61.9
pod 0 17 0.0
total 280 467 59.9


line stmt bran cond sub pod time code
1             package TestML::Runtime;
2              
3 1     1   4 use TestML::Base;
  1         1  
  1         5  
4              
5             has testml => ();
6             has bridge => ();
7             has library => ();
8             has compiler => ();
9             has skip => ();
10              
11             has function => ();
12             has error => ();
13             has global => ();
14             has base => ();
15              
16             sub BUILD {
17 1     1 0 2 my ($self) = @_;
18 1         1 $TestML::Runtime::Singleton = $self;
19 1 50 33     31 $self->{base} ||= $0 =~ m!(.*)/! ? $1 : ".";
20             }
21              
22             sub run {
23 1     1 0 2 my ($self) = @_;
24 1         7 $self->compile_testml;
25 1         249 $self->initialize_runtime;
26 1         20 $self->run_function($self->{function}, []);
27             }
28              
29             # TODO Functions should have return values
30             sub run_function {
31 1     1 0 3 my ($self, $function, $args) = @_;
32              
33 1         7 $self->apply_signature($function, $args);
34              
35 1         3 my $parent = $self->function;
36 1         3 $self->{function} = $function;
37              
38 1         2 for my $statement (@{$function->statements}) {
  1         4  
39 1 50       5 if (ref($statement) eq 'TestML::Assignment') {
40 0         0 $self->run_assignment($statement);
41             }
42             else {
43 1         8 $self->run_statement($statement);
44             }
45             }
46 1         417 $self->{function} = $parent;
47 1         5 return;
48             }
49              
50             sub apply_signature {
51 1     1 0 8 my ($self, $function, $args) = @_;
52 1         5 my $signature = $function->signature;
53              
54 1 50 33     6 die sprintf(
55             "Function received %d args but expected %d",
56             scalar(@$args),
57             scalar(@$signature),
58             ) if @$signature and @$args != @$signature;
59              
60 1         3 $function->setvar('Self', $function);
61 1         6 for (my $i = 0; $i < @$signature; $i++) {
62 0         0 my $arg = $args->[$i];
63 0 0       0 $arg = $self->run_expression($arg)
64             if ref($arg) eq 'TestML::Expression';
65 0         0 $function->setvar($signature->[$i], $arg);
66             }
67             }
68              
69             sub run_statement {
70 1     1 0 2 my ($self, $statement) = @_;
71 1   50     4 my $blocks = $self->select_blocks($statement->points || []);
72 1         3 for my $block (@$blocks) {
73 10 50       4060 $self->function->setvar('Block', $block) if $block != 1;
74 10         36 my $result = $self->run_expression($statement->expr);
75 10 50       76 if (my $assert = $statement->assert) {
76 10         48 $self->run_assertion($result, $assert);
77             }
78             }
79             }
80              
81             sub run_assignment {
82 0     0 0 0 my ($self, $assignment) = @_;
83 0         0 $self->function->setvar(
84             $assignment->name,
85             $self->run_expression($assignment->expr),
86             );
87             }
88              
89             sub run_assertion {
90 10     10 0 17 my ($self, $left, $assert) = @_;
91 10         32 my $method = 'assert_' . $assert->name;
92              
93 10         35 $self->function->getvar('TestNumber')->{value}++;
94              
95 10 50       34 if ($assert->expr) {
96 10         23 $self->$method($left, $self->run_expression($assert->expr));
97             }
98             else {
99 0         0 $self->$method($left);
100             }
101             }
102              
103             sub run_expression {
104 20     20 0 32 my ($self, $expr) = @_;
105              
106 20         27 my $context = undef;
107 20         39 $self->{error} = undef;
108 20 50       87 if ($expr->isa('TestML::Expression')) {
109 20         24 my @calls = @{$expr->calls};
  20         60  
110 20 50       69 die if @calls <= 1;
111 20         50 $context = $self->run_call(shift(@calls));
112 20         39 for my $call (@calls) {
113 40 50       122 if ($self->error) {
114             next unless
115 0 0 0     0 $call->isa('TestML::Call') and
116             $call->name eq 'Catch';
117             }
118 40         118 $context = $self->run_call($call, $context);
119             }
120             }
121             else {
122 0         0 $context = $self->run_call($expr);
123             }
124 20 50       70 if ($self->error) {
125 0         0 die $self->error;
126             }
127 20         77 return $context;
128             }
129              
130             sub run_call {
131 60     60 0 79 my ($self, $call, $context) = @_;
132              
133 60 50       362 if ($call->isa('TestML::Object')) {
134 0         0 return $call;
135             }
136 60 50       244 if ($call->isa('TestML::Function')) {
137 0         0 return $call;
138             }
139 60 100       221 if ($call->isa('TestML::Point')) {
140 20         57 return $self->get_point($call->name);
141             }
142 40 50       110 if ($call->isa('TestML::Call')) {
143 40         96 my $name = $call->name;
144 40   50     99 my $callable =
145             $self->function->getvar($name) ||
146             $self->lookup_callable($name) ||
147             die "Can't locate '$name' callable";
148 40 50       230 if ($callable->isa('TestML::Object')) {
149 0         0 return $callable;
150             }
151 40 50 66     93 return $callable unless $call->args or defined $context;
152 40   100     100 $call->{args} ||= [];
153 40         45 my $args = [map $self->run_expression($_), @{$call->args}];
  40         74  
154 40 50       152 unshift @$args, $context if $context;
155 40 50       123 if ($callable->isa('TestML::Callable')) {
156 40         54 my $value = eval { $callable->value->(@$args) };
  40         95  
157 40 50       131 if ($@) {
158 0         0 $self->{error} = $@;
159 0         0 return TestML::Error->new(value => $@);
160             }
161 40 50       162 die "'$name' did not return a TestML::Object object"
162             unless UNIVERSAL::isa($value, 'TestML::Object');
163 40         232 return $value;
164             }
165 0 0       0 if ($callable->isa('TestML::Function')) {
166 0         0 return $self->run_function($callable, $args);
167             }
168 0         0 die;
169             }
170 0         0 die;
171             }
172              
173             sub lookup_callable {
174 3     3 0 6 my ($self, $name) = @_;
175 3         3 for my $library (@{$self->function->getvar('Library')->value}) {
  3         8  
176 3 50       23 if ($library->can($name)) {
177 3     40   21 my $function = sub { $library->$name(@_) };
  40         168  
178 3         16 my $callable = TestML::Callable->new(value => $function);
179 3         12 $self->function->setvar($name, $callable);
180 3         26 return $callable;
181             }
182             }
183 0         0 return;
184             }
185              
186             sub get_point {
187 20     20 0 38 my ($self, $name) = @_;
188 20         45 my $value = $self->function->getvar('Block')->{points}{$name};
189 20 50       53 defined $value or return;
190 20 50 66     227 if ($value =~ s/\n+\z/\n/ and $value eq "\n") {
191 0         0 $value = '';
192             }
193 20         38 $value =~ s/^\\//gm;
194 20         97 return TestML::Str->new(value => $value);
195             }
196              
197             sub select_blocks {
198 1     1 0 2 my ($self, $wanted) = @_;
199 1 50       4 return [1] unless @$wanted;
200 1         3 my $selected = [];
201              
202 1         2 OUTER: for my $block (@{$self->function->data}) {
  1         4  
203 10         10 my %points = %{$block->points};
  10         27  
204 10 50       29 next if exists $points{SKIP};
205 10 50       16 if (exists $points{ONLY}) {
206 0         0 for my $point (@$wanted) {
207 0 0       0 return [] unless exists $points{$point};
208             }
209 0         0 $selected = [$block];
210 0         0 last;
211             }
212 10         19 for my $point (@$wanted) {
213 20 50       45 next OUTER unless exists $points{$point};
214             }
215 10         15 push @$selected, $block;
216 10 50       42 last if exists $points{LAST};
217             }
218 1         4 return $selected;
219             }
220              
221             sub compile_testml {
222 1     1 0 2 my ($self) = @_;
223              
224 1 50       7 die "'testml' document required but not found"
225             unless $self->testml;
226 1 50       4 if ($self->testml !~ /\n/) {
227 0 0       0 $self->testml =~ /(?:(.*)\/)?(.*)/ or die;
228 0         0 $self->{testml} = $2;
229 0 0       0 $self->{base} .= '/' . $1 if $1;
230 0         0 $self->{testml} = $self->read_testml_file($self->testml);
231             }
232 1 50       10 $self->{function} = $self->compiler->new->compile($self->testml)
233             or die "TestML document failed to compile";
234             }
235              
236             sub initialize_runtime {
237 1     1 0 4 my ($self) = @_;
238              
239 1         9 $self->{global} = $self->function->outer;
240              
241 1         7 $self->{global}->setvar(Block => TestML::Block->new);
242 1         13 $self->{global}->setvar(Label => TestML::Str->new(value => '$BlockLabel'));
243 1         5 $self->{global}->setvar(True => $TestML::Constant::True);
244 1         2 $self->{global}->setvar(False => $TestML::Constant::False);
245 1         4 $self->{global}->setvar(None => $TestML::Constant::None);
246 1         9 $self->{global}->setvar(TestNumber => TestML::Num->new(value => 0));
247 1         10 $self->{global}->setvar(Library => TestML::List->new);
248              
249 1         4 my $library = $self->function->getvar('Library');
250 1         10 for my $lib ($self->bridge, $self->library) {
251 2 100       8 if (ref($lib) eq 'ARRAY') {
252 1         16 $library->push($_->new) for @$lib;
253             }
254             else {
255 1         8 $library->push($lib->new);
256             }
257             }
258             }
259              
260             sub get_label {
261 10     10 0 35 my ($self) = @_;
262 10 50       26 my $label = $self->function->getvar('Label') or return;
263 10 50       32 $label = $label->value or return;
264 10         92 $label =~ s/\$(\w+)/$self->replace_label($1)/ge;
  10         40  
265 10         68 return $label;
266             }
267              
268             sub replace_label {
269 10     10 0 30 my ($self, $var) = @_;
270 10         29 my $block = $self->function->getvar('Block');
271 10 50       53 return $block->label if $var eq 'BlockLabel';
272 0 0       0 if (my $v = $block->points->{$var}) {
273 0         0 $v =~ s/\n.*//s;
274 0         0 $v =~ s/^\s*(.*?)\s*$/$1/;
275 0         0 return $v;
276             }
277 0 0       0 if (my $v = $self->function->getvar($var)) {
278 0         0 return $v->value;
279             }
280             }
281              
282             sub read_testml_file {
283 0     0 0 0 my ($self, $file) = @_;
284 0         0 my $path = $self->base . '/' . $file;
285 0 0       0 open my $fh, $path
286             or die "Can't open '$path' for input: $!";
287 0         0 local $/;
288 0         0 return <$fh>;
289             }
290              
291             #-----------------------------------------------------------------------------
292             package TestML::Function;
293              
294 1     1   6 use TestML::Base;
  1         1  
  1         4  
295              
296             has type => 'Func'; # Functions are TestML typed objects
297             has signature => []; # Input variable names
298             has namespace => {}; # Lexical scoped variable stash
299             has statements => []; # Exexcutable code statements
300             has data => []; # Data section scoped to this function
301              
302             my $outer = {};
303 36 100   36   188 sub outer { @_ == 1 ? $outer->{$_[0]} : ($outer->{$_[0]} = $_[1]) }
304              
305             sub getvar {
306 96     96   123 my ($self, $name) = @_;
307 96         211 while ($self) {
308 125 100       277 if (my $object = $self->namespace->{$name}) {
309 91         321 return $object;
310             }
311 34         79 $self = $self->outer;
312             }
313 5         28 undef;
314             }
315              
316             sub setvar {
317 21     21   36 my ($self, $name, $value) = @_;
318 21         58 $self->namespace->{$name} = $value;
319             }
320              
321             sub forgetvar {
322 0     0   0 my ($self, $name) = @_;
323 0         0 delete $self->namespace->{$name};
324             }
325              
326             #-----------------------------------------------------------------------------
327             package TestML::Assignment;
328              
329 1     1   6 use TestML::Base;
  1         2  
  1         4  
330              
331             has name => ();
332             has expr => ();
333              
334             #-----------------------------------------------------------------------------
335             package TestML::Statement;
336              
337 1     1   5 use TestML::Base;
  1         1  
  1         4  
338              
339             has expr => ();
340             has assert => ();
341             has points => ();
342              
343             #-----------------------------------------------------------------------------
344             package TestML::Expression;
345              
346 1     1   4 use TestML::Base;
  1         1  
  1         3  
347              
348             has calls => [];
349              
350             #-----------------------------------------------------------------------------
351             package TestML::Assertion;
352              
353 1     1   4 use TestML::Base;
  1         1  
  1         3  
354              
355             has name => ();
356             has expr => ();
357              
358             #-----------------------------------------------------------------------------
359             package TestML::Call;
360              
361 1     1   7 use TestML::Base;
  1         2  
  1         4  
362              
363             has name => ();
364             has args => ();
365              
366             #-----------------------------------------------------------------------------
367             package TestML::Callable;
368              
369 1     1   6 use TestML::Base;
  1         1  
  1         4  
370             has value => ();
371              
372             #-----------------------------------------------------------------------------
373             package TestML::Block;
374              
375 1     1   4 use TestML::Base;
  1         1  
  1         5  
376              
377             has label => '';
378             has points => {};
379              
380             #-----------------------------------------------------------------------------
381             package TestML::Point;
382              
383 1     1   5 use TestML::Base;
  1         2  
  1         3  
384              
385             has name => ();
386              
387             #-----------------------------------------------------------------------------
388             package TestML::Object;
389              
390 1     1   4 use TestML::Base;
  1         1  
  1         3  
391              
392             has value => ();
393              
394             sub type {
395 0     0   0 my $type = ref($_[0]);
396 0 0       0 $type =~ s/^TestML::// or die "Can't find type of '$type'";
397 0         0 return $type;
398             }
399              
400 0     0   0 sub str { die "Cast from ${\ $_[0]->type} to Str is not supported" }
  0         0  
401 0     0   0 sub num { die "Cast from ${\ $_[0]->type} to Num is not supported" }
  0         0  
402 0     0   0 sub bool { die "Cast from ${\ $_[0]->type} to Bool is not supported" }
  0         0  
403 0     0   0 sub list { die "Cast from ${\ $_[0]->type} to List is not supported" }
  0         0  
404 0     0   0 sub none { $TestML::Constant::None }
405              
406             #-----------------------------------------------------------------------------
407             package TestML::Str;
408              
409 1     1   5 use TestML::Base;
  1         1  
  1         3  
410             extends 'TestML::Object';
411              
412 20     20   51 sub str { $_[0] }
413 0 0   0   0 sub num { TestML::Num->new(
414             value => ($_[0]->value =~ /^-?\d+(?:\.\d+)$/ ? ($_[0]->value + 0) : 0),
415             )}
416             sub bool {
417 0 0   0   0 length($_[0]->value) ? $TestML::Constant::True : $TestML::Constant::False
418             }
419 0     0   0 sub list { TestML::List->new(value => [split //, $_[0]->value]) }
420              
421             #-----------------------------------------------------------------------------
422             package TestML::Num;
423              
424 1     1   4 use TestML::Base;
  1         2  
  1         3  
425             extends 'TestML::Object';
426              
427 0     0   0 sub str { TestML::Str->new(value => $_[0]->value . "") }
428 0     0   0 sub num { $_[0] }
429 0 0   0   0 sub bool { ($_[0]->value != 0) ? $TestML::Constant::True : $TestML::Constant::False }
430             sub list {
431 0     0   0 my $list = [];
432 0         0 $#{$list} = int($_[0]) -1;
  0         0  
433 0         0 TestML::List->new(value =>$list);
434             }
435              
436             #-----------------------------------------------------------------------------
437             package TestML::Bool;
438              
439 1     1   7 use TestML::Base;
  1         1  
  1         4  
440             extends 'TestML::Object';
441              
442 0 0   0   0 sub str { TestML::Str->new(value => $_[0]->value ? "1" : "") }
443 0 0   0   0 sub num { TestML::Num->new(value => $_[0]->value ? 1 : 0) }
444 0     0   0 sub bool { $_[0] }
445              
446             #-----------------------------------------------------------------------------
447             package TestML::List;
448              
449 1     1   6 use TestML::Base;
  1         2  
  1         3  
450             extends 'TestML::Object';
451             has value => [];
452 0     0   0 sub list { $_[0] }
453             sub push {
454 3     3   59 my ($self, $elem) = @_;
455 3         4 push @{$self->value}, $elem;
  3         7  
456             }
457              
458             #-----------------------------------------------------------------------------
459             package TestML::None;
460              
461 1     1   7 use TestML::Base;
  1         1  
  1         11  
462             extends 'TestML::Object';
463              
464 0     0     sub str { TestML::Str->new(value => '') }
465 0     0     sub num { TestML::Num->new(value => 0) }
466 0     0     sub bool { $TestML::Constant::False }
467 0     0     sub list { TestML::List->new(value => []) }
468              
469             #-----------------------------------------------------------------------------
470             package TestML::Native;
471              
472 1     1   7 use TestML::Base;
  1         2  
  1         4  
473             extends 'TestML::Object';
474              
475             #-----------------------------------------------------------------------------
476             package TestML::Error;
477              
478 1     1   6 use TestML::Base;
  1         1  
  1         5  
479             extends 'TestML::Object';
480              
481             #-----------------------------------------------------------------------------
482             package TestML::Constant;
483              
484             our $True = TestML::Bool->new(value => 1);
485             our $False = TestML::Bool->new(value => 0);
486             our $None = TestML::None->new;
487              
488             1;