File Coverage

blib/lib/TestML/Tiny.pm
Criterion Covered Total %
statement 125 146 85.6
branch 32 58 55.1
condition 22 41 53.6
subroutine 19 25 76.0
pod 0 2 0.0
total 198 272 72.7


line stmt bran cond sub pod time code
1 3     3   74136 use strict; use warnings;
  3     3   7  
  3         119  
  3         16  
  3         7  
  3         176  
2             package TestML::Tiny;
3             our $VERSION = '0.0.11';
4              
5 3     3   39 use Carp;
  3         6  
  3         290  
6 3     3   16 use Test::More;
  3         5  
  3         21  
7              
8             sub import {
9 3     3   69 strict->import;
10 3         3944 warnings->import;
11             }
12              
13             sub new {
14 2     2 0 284 my $self = bless { @_[1..$#_] }, $_[0];
15 2         12 my $testml = $self->_get_testml;
16 2         13 my $bridge = $self->_get_bridge;
17 2   33     28 $self->{runtime} ||= TestML::Tiny::Runtime->new(
18             bridge => $bridge,
19             );
20 2 50       21 my $compiler = TestML::Tiny::Compiler->new(
21             $self->{version} ? (version => $self->{version}) : (),
22             );
23 2         13 $self->{function} = $compiler->compile($testml);
24 2         28 return $self;
25             }
26              
27             sub run {
28 0     0 0 0 my ($self) = @_;
29 0   0     0 my $runtime = $self->{runtime} || '';
30 0 0 0     0 Carp::croak "Missing or invalid runtime object for TestML::Tiny::run()"
31             unless defined($runtime) and ref($runtime) eq 'TestML::Tiny::Runtime';
32 0         0 $runtime->run;
33             }
34              
35             sub _get_testml {
36 2     2   6 my ($self) = @_;
37 2 50       25 my $testml = $self->{testml}
38             or Carp::croak "TestML object requires a testml attribute";
39 2 50       18 $testml = $self->_slurp($testml)
40             if $testml !~ /\n/;
41 2         6 return $testml;
42             }
43              
44             sub _get_bridge {
45 2     2   26 my ($self) = @_;
46 2   50     25 my $bridge = $self->{bridge} || 'main';
47 2 50       8 return $bridge if ref $bridge;
48 2         241 eval "require $bridge";
49 2 50 33     34 Carp::croak $@ if $@ and $@ !~ /^Can't locate /;
50             return (
51 2 50       4 defined(&{"${bridge}::new"})
  2         22  
52             ? $bridge->new
53             : bless {}, $bridge
54             );
55             }
56              
57             sub _slurp {
58 0 0   0   0 open my $fh, "<:raw:encoding(UTF-8)", $_[1]
59             or die "Can't open $_[1] for input";
60 0         0 local $/;
61 0         0 <$fh>;
62             }
63              
64             #------------------------------------------------------------------------------
65             =pod comment
66              
67             ::Runtime -- Run a TestML Function
68              
69             The TestML Code and Data get compiled into a Function, and the Function is run
70             by this Runtime class. Typically data is manipulated by Bridge functions, and
71             at some point Assertions are made. The assertions are the things that call
72             Test::More::is and Test::More::ok.
73             =cut
74              
75             package TestML::Tiny::Runtime;
76              
77             sub new {
78 2     2   25 my $self = $TestML::Tiny::Runtime::Singleton =
79             bless { @_[1..$#_] }, $_[0];
80             };
81              
82             sub run {
83 0     0   0 Test::More::fail 'not done yet!';
84 0         0 Test::More::done_testing;
85             }
86              
87             #------------------------------------------------------------------------------
88             =pod comment
89              
90             ::Compiler -- Turn a TestML document into a runnable TestML Function.
91              
92             A TestML "document" is comprised of 3 main parts: Meta, Code, Data. This
93             information often is in a single TestML (.tml) file or string, but it doesn't
94             need to be. The information can come from anywhere and be in any form that is
95             supported; it just must all be present when it is needed.
96              
97             The Meta information must be known first. It dictates where the Code and Data
98             come from, and in what format they are. Also the Code and Data formats depend
99             on the TestML API Version that is supplied. Before the Code and Data can be
100             compiled, a Version must be supplied (no default) and then the compiler must
101             support that Version. This allows TestML to change over time with no
102             confusion.
103              
104             The compile function returns a Function object, which in turn contains an
105             array of Statements and an array of Data Blocks. This function is the run by
106             the Runtime object.
107             =cut
108             package TestML::Tiny::Compiler;
109              
110             my $ID = qr/\w+/;
111             my $SP = qr/[\ \t]/;
112             my $LINE = qr/.*$/m;
113             my $DIRECTIVE = qr/^%($ID)$SP+($LINE)/m;
114              
115             sub new {
116 2     2   12 my $self = bless { @_[1..$#_] }, $_[0];
117             }
118              
119             sub runtime {
120 0     0   0 $TestML::Tiny::Runtime::Singleton;
121             }
122              
123             sub compile {
124 2     2   5 my ($self, $testml) = @_;
125 2         16 my $function = $self->{function} = TestML::Tiny::Function->new;
126 2         6 $self->{testml} = $testml;
127 2         9 $self->preprocess;
128 2         9 my $version = $self->check_version;
129 2         6 my ($code_syntax, $data_syntax) =
130 2         5 @{$self}{qw(code_syntax data_syntax)};
131 2         6 my $code_method = "compile_code_${code_syntax}_$version";
132 2 50       21 Carp::croak "Don't know how to compile TestML '$code_syntax' code"
133             unless $self->can($code_method);
134 2         7 my $data_method = "compile_data_${data_syntax}_$version";
135 2 50       11 Carp::croak "Don't know how to compile TestML '$data_syntax' data"
136             unless $self->can($data_method);
137 2         7 $function->{statements} = $self->$code_method;
138 2         11 $function->{data} = $self->$data_method;
139 2         7 return $function;
140             }
141              
142             my %directives = (
143             code_syntax => 'tiny',
144             data_syntax => 'testml',
145             data_marker => '===',
146             block_marker => '===',
147             point_marker => '---',
148             );
149             sub preprocess {
150 2     2   4 my ($self) = @_;
151              
152 2   50     14 my $version = $self->{version} || undef;
153 2         4 my $testml = $self->{testml};
154 2         28 my $directives = [ $testml =~ /$DIRECTIVE/gm ];
155 2         133 $testml =~ s/($DIRECTIVE)/#$1/g;
156 2         11 while (@$directives) {
157 4         13 my ($key, $value) = splice(@$directives, 0, 2);
158 4 100       1870 if ($key eq "TestML") {
    100          
    50          
    0          
    0          
159 2         10 $self->check_not_set_and_set($key, $value, 'version');
160             }
161             elsif ($key eq "BlockMarker") {
162 1         167 $self->check_not_set_and_set(
163             'BlockMarker', $value, 'block_marker'
164             );
165 1         17 ($self->{block_marker} = $value) =~
166             s/([\*\^\$\+\?\(\)\.])/\\$1/g;
167             }
168             elsif ($key eq "PointMarker") {
169 1         3 $self->check_not_set_and_set(
170             'PointMarker', $value, 'point_marker'
171             );
172 1         11 ($self->{point_marker} = $value) =~
173             s/([\*\^\$\+\?\(\)\.])/\\$1/g;
174             }
175             elsif ($key eq "CodeSyntax") {
176 0         0 die "Untested";
177 0         0 $self->check_not_set_and_set(
178             'CodeSyntax', $value, 'code_syntax'
179             );
180 0         0 $self->{code_syntax} = $value;
181             }
182             elsif ($key eq "DataSyntax") {
183 0         0 die "Untested";
184 0         0 $self->check_not_set_and_set(
185             'DataSyntax', $value, 'data_syntax'
186             );
187 0         0 $self->{data_syntax} = $value;
188             }
189             else {
190 0         0 Carp::croak "Unknown TestML directive: '%$key'";
191             }
192             }
193 2 100 66     591 $self->{data_marker} = $self->{block_marker}
194             if not($self->{data_marker}) and $self->{block_marker};
195 2         12 for my $directive (keys %directives) {
196 10   66     41 $self->{$directive} ||= $directives{$directive};
197             }
198              
199 2         60 ($self->{code}, $self->{data}) =
200             ($testml =~ /(.*?)(^$self->{data_marker}.*)/msg);
201 2   50     7 $self->{code} ||= '';
202 2   50     11 $self->{data} ||= '';
203             }
204              
205             sub check_not_set_and_set {
206 4     4   14 my ($self, $key, $value, $attr) = @_;
207 4 50 33     26 if (defined $self->{$attr} and $self->{$attr} ne $value) {
208 0         0 Carp::croak "Can't set TestML '$key' directive to '$value'. " .
209             "Already set to '$self->{$attr}'";
210             }
211 4         18 $self->{$attr} = $value;
212             }
213              
214             sub check_version {
215 2     2   5 my ($self) = @_;
216 2   50     8 my $version = $self->{version} || undef;
217 2 50       6 Carp::croak "TestML syntax version not defined. Cannot continue"
218             unless defined $version;
219 2 50       11 Carp::croak "Invalid value for TestML version '$version'. Must be 0.1.0"
220             unless $version eq '0.1.0';
221 2         9 $version =~ s/\./_/g;
222 2         6 return $version;
223             }
224              
225             sub compile_code_tiny_0_1_0 {
226 2     2   4 my ($self) = @_;
227 2         3 my $num = 1;
228 2         47 [ grep { not /(^#|^\s*$)/ } split /\n/, $self->{code} ];
  9         47  
229             }
230              
231             sub compile_data_testml_0_1_0 {
232 2     2   3 my ($self) = @_;
233              
234 2         15 my $lines = [ grep { ! /^#/ } split /\n/, $self->{data} ];
  24         43  
235              
236 2         6 my $blocks = [];
237 2         3 my $parse = [];
238 2         5 push @$lines, undef; # sentinel
239 2         9 while (@$lines) {
240 24         35 push @$parse, shift @$lines;
241 24 100 100     131 if (!defined($lines->[0]) or
242             $lines->[0] =~ /^$self->{block_marker}/
243             ) {
244 4         10 my $block = $self->_parse_testml_block($parse);
245 4 50       13 push @$blocks, $block
246             unless exists $block->{SKIP};
247 4 50       145 last if exists $block->{LAST};
248 4         7 $parse = []; # clear for next parse
249             }
250 24 100       73 last if !defined($lines->[0]);
251             }
252              
253 2         5 my $only = [ grep { exists $_->{ONLY} } @$blocks ];
  4         10  
254              
255 2 50       11 return @$only ? $only : $blocks;
256             }
257              
258             sub _parse_testml_block {
259 4     4   7 my ($self, $lines) = @_;
260              
261 4         45 my ($label) = $lines->[0] =~ /^$self->{block_marker}(?:\s+(.*))?$/;
262 4   66     69 shift @$lines until not(@$lines) or
263             $lines->[0] =~ /^$self->{point_marker} +\w+/;
264              
265 4         12 my $block = $self->_parse_testml_points($lines);
266 4   100     55 $block->{Label} = $label || '';
267              
268 4         10 return $block;
269             }
270              
271             sub _parse_testml_points {
272 4     4   6 my ($self, $lines) = @_;
273              
274 4         7 my $block = {};
275              
276 4         10 while (@$lines) {
277 11         16 my $line = shift @$lines;
278 11 50       68 $line =~ /^$self->{point_marker} +(\w+)/
279             or die "Invalid TestML line:\n'$line'";
280 11         17 my $point_name = $1;
281 11 50       23 die "$block repeats $point_name"
282             if exists $block->{$point_name};
283 11         20 $block->{$point_name} = '';
284 11 100       97 if ($line =~ /^$self->{point_marker} +(\w+): +(.*?) *$/) {
    50          
285 7         40 ($block->{$1} = $2) =~ s/^ *(.*?) *$/$1/;
286 7   100     109 shift @$lines while @$lines and
287             $lines->[0] !~ /^$self->{point_marker} +(\w)/;
288             }
289             elsif ($line =~ /^$self->{point_marker} +(\w+)$/) {
290 4         5 $point_name = $1;
291 4         9 while ( @$lines ) {
292 9         11 $line = shift @$lines;
293 9 100       36 if ($line =~ /^$self->{point_marker} \w+/) {
294 2         8 unshift @$lines, $line;
295 2         4 last;
296             }
297 7         15 $block->{$point_name} .= "$line\n";
298             }
299 4         14 $block->{$point_name} =~ s/\n\s*\z/\n/;
300 4         14 $block->{$point_name} =~ s/^\\//gm;
301             }
302             else {
303 0         0 die "Invalid TestML line:\n'$line'";
304             }
305             }
306 4         313 return $block;
307             }
308              
309             #------------------------------------------------------------------------------
310             =pod comment
311              
312             A Function is just an array of "executable" statements that are proceseded in
313             order. Some of the statements maybe be function declarations and function
314             calls. The Compiler produces a top level scope function, with a Data set, and a
315             Namespace for variables.
316              
317             All functions are anonymous, but they can be assigned to variables, and then
318             you can call that variable name.
319             =cut
320             package TestML::Tiny::Function;
321              
322             sub new {
323 2     2   142 my $self = bless {
324             statements => [],
325             data => [],
326             namespace => {},
327             }, $_[0];
328             }
329              
330             #------------------------------------------------------------------------------
331             package TestML::Tiny::Bridge;
332              
333             sub new {
334 0     0     my $self = bless { @_[1..$#_] }, $_[0];
335             }
336              
337             #------------------------------------------------------------------------------
338             package TestML::Tiny::Library::Standard;
339              
340             sub new {
341 0     0     my $self = bless { @_[1..$#_] }, $_[0];
342             }
343              
344             1;