File Coverage

inc/lib/TestML/Run.pm
Criterion Covered Total %
statement 172 337 51.0
branch 58 160 36.2
condition 10 34 29.4
subroutine 29 55 52.7
pod 0 48 0.0
total 269 634 42.4


line stmt bran cond sub pod time code
1 2     2   14 use strict; use warnings;
  2     2   3  
  2         61  
  2         6  
  2         3  
  2         196  
2             package TestMLFunction;
3              
4             sub new {
5 0     0   0 my ($class, $func) = @_;
6 0         0 return bless {func => $func}, $class;
7             }
8              
9             package TestML::Run;
10              
11 2     2   1749 use JSON::PP;
  2         38830  
  2         185  
12              
13 2     2   1147 use utf8;
  2         562  
  2         11  
14 2     2   838 use TestML::Boolean;
  2         6  
  2         136  
15 2     2   13 use Scalar::Util;
  2         4  
  2         11026  
16              
17             # use XXX;
18              
19             my $vtable = {
20             '==' => 'assert_eq',
21             '~~' => 'assert_has',
22             '=~' => 'assert_like',
23             '!==' => 'assert_not_eq',
24             '!~~' => 'assert_not_has',
25             '!=~' => 'assert_not_like',
26              
27             '.' => 'exec_dot',
28             '%' => 'each_exec',
29             '%<>' => 'each_pick',
30             '<>' => 'pick_exec',
31             '&' => 'call_func',
32              
33             '"' => 'get_str',
34             ':' => 'get_hash',
35             '[]' => 'get_list',
36             '*' => 'get_point',
37              
38             '=' => 'set_var',
39             '||=' => 'or_set_var',
40             };
41              
42             my $types = {
43             '=>' => 'func',
44             '/' => 'regex',
45             '!' => 'error',
46             '?' => 'native',
47             };
48              
49             #------------------------------------------------------------------------------
50             sub new {
51 2     2 0 5 my ($class, %params) = @_;
52              
53 2         5 my $testml = $params{testml};
54              
55             return bless {
56             file => $params{file},
57             ast => $params{testml},
58              
59             bridge => $params{bridge},
60             stdlib => $params{stdlib},
61              
62 2         14 vars => {},
63             block => undef,
64             warned_only => false,
65             error => undef,
66             thrown => undef,
67             }, $class;
68             }
69              
70             sub from_file {
71 2     2 0 6 my ($self, $file) = @_;
72              
73 2         5 $self->{file} = $file;
74              
75 2 50       79 open INPUT, $file
76             or die "Can't open '$file' for input";
77              
78 2         6 $self->{ast} = decode_json do { local $/; };
  2         9  
  2         93  
79              
80 2         13294 return $self;
81             }
82              
83             sub test {
84 2     2 0 5 my ($self) = @_;
85              
86 2         24 $self->testml_begin;
87              
88 2         3 for my $statement (@{$self->{ast}{code}}) {
  2         7  
89 2         22 $self->exec_expr($statement);
90             }
91              
92 2         14 $self->testml_end;
93              
94 2         5 return;
95             }
96              
97             #------------------------------------------------------------------------------
98             sub exec {
99 46     46 0 77 my ($self, $expr) = @_;
100              
101 46         143 $self->exec_expr($expr)->[0];
102             }
103              
104             sub exec_expr {
105 72     72 0 144 my ($self, $expr, $context) = @_;
106              
107 72 100       127 $context = [] unless defined $context;
108              
109 72 100       140 return [$expr] unless $self->type($expr) eq 'expr';
110              
111 41         84 my @args = @$expr;
112 41         43 my @ret;
113 41         60 my $name = shift @args;
114 41         54 my $opcode = $name;
115 41 100       101 if (my $call = $vtable->{$opcode}) {
116 35 50       88 $call = $call->[0] if ref($call) eq 'ARRAY';
117 35         134 @ret = $self->$call(@args);
118             }
119             else {
120 6         20 unshift @args, $_ for reverse @$context;
121              
122 6 50       28 if (defined(my $value = $self->{vars}{$name})) {
    50          
    0          
123 0 0       0 if (@args) {
124 0 0       0 die "Variable '$name' has args but is not a function"
125             unless $self->type($value) eq 'func';
126 0         0 @ret = $self->exec_func($value, \@args);
127             }
128             else {
129 0         0 @ret = ($value);
130             }
131             }
132             elsif ($name =~ /^[a-z]/) {
133 6         69 @ret = $self->call_bridge($name, @args);
134             }
135             elsif ($name =~ /^[A-Z]/) {
136 0         0 @ret = $self->call_stdlib($name, @args);
137             }
138             else {
139 0         0 die "Can't resolve TestML function '$name'";
140             }
141             }
142              
143 41         209 return [@ret];
144             }
145              
146             sub exec_func {
147 0     0 0 0 my ($self, $function, $args) = @_;
148 0 0       0 $args = [] unless defined $args;
149              
150 0         0 my ($op, $signature, $statements) = @$function;
151              
152 0 0 0     0 if (@$signature > 1 and @$args == 1 and $self->type($args) eq 'list') {
      0        
153 0         0 $args = $args->[0];
154             }
155              
156 0 0       0 die "TestML function expected '${\scalar @$signature}' arguments, but was called with '${\scalar @$args}' arguments"
  0         0  
  0         0  
157             if @$signature != @$args;
158              
159 0         0 my $i = 0;
160 0         0 for my $v (@$signature) {
161 0         0 $self->{vars}{$v} = $self->exec($args->[$i++]);
162             }
163              
164 0         0 for my $statement (@$statements) {
165 0         0 $self->exec_expr($statement);
166             }
167              
168 0         0 return;
169             }
170              
171             #------------------------------------------------------------------------------
172             sub call_bridge {
173 6     6 0 15 my ($self, $name, @args) = @_;
174              
175 6 100       47 if (not $self->{bridge}) {
176 2   50     10 my $bridge_module = $ENV{TESTML_BRIDGE} || 'TestMLBridge';
177              
178 2 50       8 if (my $code = $self->{ast}{bridge}{perl}) {
179 0 0       0 eval <<"..." or die $@;
180             use strict; use warnings;
181             package TestMLBridge;
182             use base 'TestML::Bridge';
183             $code;
184             1;
185             ...
186             }
187             else {
188 2 50       160 eval "require $bridge_module; 1" or die $@;
189             }
190              
191 2         23 $self->{bridge} = $bridge_module->new;
192             }
193              
194 6         31 (my $call = $name) =~ s/-/_/g;
195              
196             die "Can't find bridge function: '$name'"
197 6 50 33     50 unless $self->{bridge} and $self->{bridge}->can($call);
198              
199 6         12 @args = map {$self->uncook($self->exec($_))} @args;
  9         23  
200              
201 6         21 my @ret = $self->{bridge}->$call(@args);
202              
203 6 50       558 return unless @ret;
204              
205 6         68 $self->cook($ret[0]);
206             }
207              
208             sub call_stdlib {
209 0     0 0 0 my ($self, $name, @args) = @_;
210              
211 0 0       0 if (not $self->{stdlib}) {
212 0         0 require TestML::StdLib;
213 0         0 $self->{stdlib} = TestML::StdLib->new($self);
214             }
215              
216 0         0 my $call = lc $name;
217             die "Unknown TestML Standard Library function: '$name'"
218 0 0       0 unless $self->{stdlib}->can($call);
219              
220 0         0 @args = map {$self->uncook($self->exec($_))} @args;
  0         0  
221              
222 0         0 $self->cook($self->{stdlib}->$call(@args));
223             }
224              
225             #------------------------------------------------------------------------------
226             sub assert_eq {
227 3     3 0 8 my ($self, $left, $right, $label, $not) = @_;
228 3         42 my $got = $self->{vars}{Got} = $self->exec($left);
229 3         5 my $want = $self->{vars}{Want} = $self->exec($right);
230 3         8 my $method = $self->get_method('assert_%s_eq_%s', $got, $want);
231 3         8 $self->$method($got, $want, $label, $not);
232 3         5 return;
233             }
234              
235             sub assert_str_eq_str {
236 3     3 0 4 my ($self, $got, $want, $label, $not) = @_;
237 3         8 $self->testml_eq($got, $want, $self->get_label($label), $not);
238             }
239              
240             sub assert_num_eq_num {
241 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
242 0         0 $self->testml_eq($got, $want, $self->get_label($label), $not);
243             }
244              
245             sub assert_bool_eq_bool {
246 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
247 0         0 $self->testml_eq($got, $want, $self->get_label($label), $not);
248             }
249              
250              
251             sub assert_has {
252 0     0 0 0 my ($self, $left, $right, $label, $not) = @_;
253 0         0 my $got = $self->exec($left);
254 0         0 my $want = $self->exec($right);
255 0         0 my $method = $self->get_method('assert_%s_has_%s', $got, $want);
256 0         0 $self->$method($got, $want, $label, $not);
257 0         0 return;
258             }
259              
260             sub assert_str_has_str {
261 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
262 0         0 $self->{vars}{Got} = $got;
263 0         0 $self->{vars}{Want} = $want;
264 0         0 $self->testml_has($got, $want, $self->get_label($label), $not);
265             }
266              
267             sub assert_str_has_list {
268 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
269 0         0 for my $str (@{$want->[0]}) {
  0         0  
270 0         0 $self->assert_str_has_str($got, $str, $label, $not);
271             }
272             }
273              
274             sub assert_list_has_str {
275 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
276 0         0 $self->{vars}{Got} = $got;
277 0         0 $self->{vars}{Want} = $want;
278 0         0 $self->testml_list_has($got->[0], $want, $self->get_label($label), $not);
279             }
280              
281             sub assert_list_has_list {
282 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
283 0         0 for my $str (@{$want->[0]}) {
  0         0  
284 0         0 $self->assert_list_has_str($got, $str, $label, $not);
285             }
286             }
287              
288              
289             sub assert_like {
290 3     3 0 9 my ($self, $left, $right, $label, $not) = @_;
291 3         8 my $got = $self->exec($left);
292 3         15 my $want = $self->exec($right);
293 3         23 my $method = $self->get_method('assert_%s_like_%s', $got, $want);
294 3         16 $self->$method($got, $want, $label, $not);
295 3         20 return;
296             }
297              
298             sub assert_str_like_regex {
299 7     7 0 17 my ($self, $got, $want, $label, $not) = @_;
300 7         23 $self->{vars}{Got} = $got;
301 7         13 $self->{vars}{Want} = "/${\ $want->[1]}/";
  7         23  
302 7         20 $want = $self->uncook($want);
303 7         34 $self->testml_like($got, $want, $self->get_label($label), $not);
304             }
305              
306             sub assert_str_like_list {
307 3     3 0 10 my ($self, $got, $want, $label, $not) = @_;
308 3         5 for my $regex (@{$want->[0]}) {
  3         15  
309 7         28 $self->assert_str_like_regex($got, $regex, $label, $not);
310             }
311             }
312              
313             sub assert_list_like_regex {
314 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
315 0         0 for my $str (@{$got->[0]}) {
  0         0  
316 0         0 $self->assert_str_like_regex($str, $want, $label, $not);
317             }
318             }
319              
320             sub assert_list_like_list {
321 0     0 0 0 my ($self, $got, $want, $label, $not) = @_;
322 0         0 for my $str (@{$got->[0]}) {
  0         0  
323 0         0 for my $regex (@{$want->[0]}) {
  0         0  
324 0         0 $self->assert_str_like_regex($str, $regex, $label, $not);
325             }
326             }
327             }
328              
329             sub assert_not_eq {
330 0     0 0 0 my ($self, $got, $want, $label) = @_;
331 0         0 $self->assert_eq($got, $want, $label, true);
332             }
333              
334             sub assert_not_has {
335 0     0 0 0 my ($self, $got, $want, $label) = @_;
336 0         0 $self->assert_has($got, $want, $label, true);
337             }
338              
339             sub assert_not_like {
340 0     0 0 0 my ($self, $got, $want, $label) = @_;
341 0         0 $self->assert_like($got, $want, $label, true);
342             }
343              
344             #------------------------------------------------------------------------------
345             sub exec_dot {
346 6     6 0 24 my ($self, @args) = @_;
347              
348 6         10 my $context = [];
349              
350 6         11 delete $self->{error};
351 6         11 for my $call (@args) {
352 12 50       26 if (not $self->{error}) {
353 12         18 eval {
354 12 50       21 if ($self->type($call) eq 'func') {
355 0         0 $self->exec_func($call, $context->[0]);
356 0         0 $context = [];
357             }
358             else {
359 12         26 $context = $self->exec_expr($call, $context);
360             }
361             };
362 12 50       58 if ($@) {
    50          
363 0 0       0 if ($ENV{TESTML_DEVEL}) {
364 0         0 require Carp;
365 0         0 Carp::cluck($@);
366             }
367 0         0 $self->{error} = $self->call_stdlib('Error', "$@");
368             }
369             elsif ($self->{thrown}) {
370 0         0 $self->{error} = $self->cook(delete $self->{thrown});
371             }
372             }
373             else {
374 0 0       0 if ($call->[0] eq 'Catch') {
375 0         0 $context = [delete $self->{error}];
376             }
377             }
378             }
379              
380 0         0 die "Uncaught Error: ${\ $self->{error}[1]{msg}}"
381 6 50       17 if $self->{error};
382              
383 6         26 return @$context;
384             }
385              
386             sub each_exec {
387 0     0 0 0 my ($self, $list, $expr) = @_;
388 0         0 $list = $self->exec($list);
389 0         0 $expr = $self->exec($expr);
390              
391 0         0 for my $item (@{$list->[0]}) {
  0         0  
392 0         0 $self->{vars}{_} = [$item];
393 0 0       0 if ($self->type($expr) eq 'func') {
394 0 0       0 if (@{$expr->[1]} == 0) {
  0         0  
395 0         0 $self->exec_func($expr);
396             }
397             else {
398 0         0 $self->exec_func($expr, [$item]);
399             }
400             }
401             else {
402 0         0 $self->exec_expr($expr);
403             }
404             }
405             }
406              
407             sub each_pick {
408 2     2 0 5 my ($self, $list, $expr) = @_;
409              
410 2         5 for my $block (@{$self->{ast}{data}}) {
  2         6  
411 6         15 $self->{block} = $block;
412              
413 6 50 33     19 if (defined $block->{ONLY} and not $self->{warned_only}) {
414 0         0 $self->err("Warning: TestML 'ONLY' in use.");
415 0         0 $self->{warned_only} = true;
416             }
417              
418 6         24 $self->exec_expr(['<>', $list, $expr]);
419             }
420              
421 2         106 delete $self->{block};
422              
423 2         10 return;
424             }
425              
426             sub pick_exec {
427 6     6 0 13 my ($self, $list, $expr) = @_;
428              
429 6         7 my $pick = 1;
430 6 50       16 if (my $when = $self->{block}{WHEN}) {
431 0 0       0 if ($when =~ /^Env:(\w+)$/) {
432 0 0       0 $pick = 0 unless $ENV{$1};
433             }
434             }
435              
436 6 50       13 if ($pick) {
437 6         20 for my $point (@$list) {
438 12 50 33     164 if (
      33        
      33        
439             ($point =~ /^\*/ and
440             not exists $self->{block}{substr($point, 1)}) or
441             ($point =~ /^!*/) and
442             exists $self->{block}{substr($point, 2)}
443             ) {
444 0         0 $pick = 0;
445 0         0 last;
446             }
447             }
448             }
449              
450 6 50       14 if ($pick) {
451 6 50       13 if ($self->type($expr) eq 'func') {
452 0         0 $self->exec_func($expr);
453             }
454             else {
455 6         56 $self->exec_expr($expr);
456             }
457             }
458              
459 6         20 return;
460             }
461              
462             sub call_func {
463 0     0 0 0 my ($self, $func) = @_;
464 0         0 my $name = $func->[0];
465 0         0 $func = $self->exec($func);
466 0 0 0     0 die "Tried to call '$name' but is not a function"
467             unless defined $func and $self->type($func) eq 'func';
468 0         0 $self->exec_func($func);
469             }
470              
471             sub get_str {
472 0     0 0 0 my ($self, $string) = @_;
473 0         0 $self->interpolate($string);
474             }
475              
476             sub get_hash {
477 0     0 0 0 my ($self, $hash, $key) = @_;
478 0         0 $hash = $self->exec($hash);
479 0         0 $key = $self->exec($key);
480 0         0 $self->cook($hash->[0]{$key});
481             }
482              
483             sub get_list {
484 0     0 0 0 my ($self, $list, $index) = @_;
485 0         0 $list = $self->exec($list);
486 0 0       0 return [] if not @{$list->[0]};
  0         0  
487 0         0 $self->cook($list->[0][$index]);
488             }
489              
490             sub get_point {
491 15     15 0 27 my ($self, $name) = @_;
492 15         59 $self->getp($name);
493             }
494              
495             sub set_var {
496 0     0 0 0 my ($self, $name, $expr) = @_;
497              
498 0         0 $self->setv($name, $self->exec($expr));
499              
500 0         0 return;
501             }
502              
503             sub or_set_var {
504 0     0 0 0 my ($self, $name, $expr) = @_;
505 0 0       0 return if defined $self->{vars}{$name};
506              
507 0 0       0 if ($self->type($expr) eq 'func') {
508 0         0 $self->setv($name, $expr);
509             }
510             else {
511 0         0 $self->setv($name, $self->exec($expr));
512             }
513 0         0 return;
514             }
515              
516             #------------------------------------------------------------------------------
517             sub getp {
518 15     15 0 23 my ($self, $name) = @_;
519 15 50       30 return unless $self->{block};
520 15         45 my $value = $self->{block}{$name};
521 15 50       51 $self->exec($value) if defined $value;
522             }
523              
524             sub getv {
525 7     7 0 19 my ($self, $name) = @_;
526 7         64 $self->{vars}{$name};
527             }
528              
529             sub setv {
530 0     0 0 0 my ($self, $name, $value) = @_;
531 0         0 $self->{vars}{$name} = $value;
532 0         0 return;
533             }
534              
535             #------------------------------------------------------------------------------
536             sub type {
537 118     118 0 153 my ($self, $value) = @_;
538              
539 118 50       170 return 'null' if not defined $value;
540              
541 118 100       201 if (not ref $value) {
542 46 50       131 return 'num' if Scalar::Util::looks_like_number($value);
543 46         3679 return 'str';
544             }
545 72 50       154 return 'bool' if isBoolean($value);
546 72 50       148 if (ref($value) eq 'ARRAY') {
547 72 50       121 return 'none' if @$value == 0;
548 72 100       184 return $_ if $_ = $types->{$value->[0]};
549 65 100       152 return 'list' if ref($value->[0]) eq 'ARRAY';
550 59 50       92 return 'hash' if ref($value->[0]) eq 'HASH';
551 59         146 return 'expr';
552             }
553              
554 0         0 require XXX;
555 0         0 XXX::ZZZ("Can't determine type of this value:", $value);
556             }
557              
558             sub cook {
559 6     6 0 24 my ($self, @value) = @_;
560              
561 6 50       20 return [] if not @value;
562 6         13 my $value = $value[0];
563 6 50       23 return undef if not defined $value;
564              
565 6 50       43 return $value if not ref $value;
566 0 0       0 return [$value] if ref($value) =~ /^(?:HASH|ARRAY)$/;
567 0 0       0 return $value if isBoolean($value);
568 0 0       0 return ['/', $value] if ref($value) eq 'Regexp';
569 0 0       0 return ['!', $value] if ref($value) eq 'TestMLError';
570 0 0       0 return $value->{func} if ref($value) eq 'TestMLFunction';
571 0         0 return ['?', $value];
572             }
573              
574             sub uncook {
575 16     16 0 26 my ($self, $value) = @_;
576              
577 16         32 my $type = $self->type($value);
578              
579 16 100       97 return $value if $type =~ /^(?:str|num|bool|null)$/;
580 7 50       25 return $value->[0] if $type =~ /^(?:list|hash)$/;
581 7 50       23 return $value->[1] if $type =~ /^(?:error|native)$/;
582 7 50       17 return TestMLFunction->new($value) if $type eq 'func';
583 7 50       20 if ($type eq 'regex') {
584 7 50       25 return ref($value->[1]) eq 'Regexp'
585             ? $value->[1]
586 7         193 : qr/${\ $value->[1]}/;
587             }
588 0 0       0 return () if $type eq 'none';
589              
590 0         0 require XXX;
591 0         0 XXX::ZZZ("Can't uncook this value of type '$type':", $value);
592             }
593              
594             #------------------------------------------------------------------------------
595             sub get_method {
596 6     6 0 20 my ($self, $pattern, @args) = @_;
597              
598 6         19 my $method = sprintf $pattern, map $self->type($_), @args;
599              
600 6 50       90 die "Method '$method' does not exist" unless $self->can($method);
601              
602 6         21 return $method;
603             }
604              
605             sub get_label {
606 10     10 0 41 my ($self, $label_expr) = @_;
607 10 100       28 $label_expr = '' unless defined $label_expr;
608              
609 10         24 my $label = $self->exec($label_expr);
610              
611 10   50     60 $label ||= $self->getv('Label') || '';
      66        
612              
613             my $block_label = (
614             defined($self->{block}) and
615             defined($self->{block}{Label})
616             ) ? $self->{block}{Label}
617 10 50 33     54 : '';
618              
619 10 100       23 if ($label) {
620 3         5 $label =~ s/^\+/$block_label/;
621 3         13 $label =~ s/\+$/$block_label/;
622 3         6 $label =~ s/\{\+\}/$block_label/;
623             }
624             else {
625 7         12 $label = $block_label;
626 7 50       36 $label = '' unless defined $label;
627             }
628              
629 10         34 return $self->interpolate($label, true);
630             }
631              
632             sub interpolate {
633 10     10 0 19 my ($self, $string, $label) = @_;
634             # XXX Hack to see input file in label:
635 10         28 $self->{vars}{File} = $ENV{TESTML_FILEVAR};
636              
637 10         21 $string =~ s/\{([\-\w]+)\}/$self->transform1($1, $label)/ge;
  0         0  
638 10         15 $string =~ s/\{\*([\-\w]+)\}/$self->transform2($1, $label)/ge;
  0         0  
639              
640 10         67 return $string;
641             }
642              
643             sub transform {
644 0     0 0   my ($self, $value, $label) = @_;
645 0           my $type = $self->type($value);
646 0 0         if ($label) {
647 0 0         if ($type =~ /^(?:list|hash)$/) {
648 0           return encode_json($value->[0]);
649             }
650 0 0         if ($type eq 'regex') {
651 0           return "$value->[1]";
652             }
653 0           $value =~ s/\n/␤/g;
654 0           return "$value";
655             }
656             else {
657 0 0         if ($type =~ /^(?:list|hash)$/) {
658 0           return encode_json($value->[0]);
659             }
660             else {
661 0           return "$value";
662             }
663             }
664             }
665              
666             sub transform1 {
667 0     0 0   my ($self, $name, $label) = @_;
668 0           my $value = $self->{vars}{$name};
669 0 0         return '' unless defined $value;
670 0           $self->transform($value, $label);
671             }
672              
673             sub transform2 {
674 0     0 0   my ($self, $name, $label) = @_;
675 0 0         return '' unless $self->{block};
676 0           my $value = $self->{block}{$name};
677 0 0         return '' unless defined $value;
678 0           $self->transform($value, $label);
679             }
680              
681             1;