1 # non-package OptreeCheck.pm
2 # pm allows 'use OptreeCheck', which also imports
3 # no package decl means all functions defined into main
4 # otherwise, it's like "require './test.pl'"
8 OptreeCheck - check optrees
12 OptreeCheck supports regression testing of perl's parser, optimizer,
13 bytecode generator, via a single function: checkOptree(%args).'
15 checkOptree(name => "your title here", # optional, (synth from others)
16 bcopts => '-exec', # $opt or \@opts, passed to BC::compile
17 code => sub {my $a}, # coderef, or source (wrapped and evald)
18 # prog => 'sort @a', # run in subprocess, aka -MO=Concise
19 # skip => 1, # skips test
20 # todo => 'excuse', # anticipated failures
21 # fail => 1 # fails (by redirecting result)
22 # debug => 1, # turns on regex debug for match test !!
23 # retry => 1 # retry with debug on test failure
24 expect => <<'EOT_EOT', expect_nt => <<'EONT_EONT');
25 # 1 <;> nextstate(main 45 optree.t:23) v
26 # 2 <0> padsv[$a:45,46] M/LVINTRO
27 # 3 <1> leavesub[1 ref] K/REFC,1
29 # 1 <;> nextstate(main 45 optree.t:23) v
30 # 2 <0> padsv[$a:45,46] M/LVINTRO
31 # 3 <1> leavesub[1 ref] K/REFC,1
34 =head1 checkOptree(%in) Overview
36 Calls getRendering(), which runs code or prog through B::Concise, and
37 captures its rendering.
39 Calls mkCheckRex() to produce a regex which will match the expected
40 rendering, and fail when it doesn't match.
42 Also calls like($rendering,/$regex/,$name), and thereby plugs into the
45 =head1 checkOptree(%Args) API
47 Accepts %Args, with following requirements and actions:
49 expect and expect_nt are both: required, not empty, not whitespace.
50 It's a fatal error otherwise, because false positives are BAD.
52 Either code or prog must be present. prog is some source code, and is
53 passed through via runperl, to B::Concise like this: (bcopts are fixed
56 './perl -w -MO=Concise,$bcopts_massaged -e $src'
58 code is a subref, or $src, like above. If it's not a subref, it's
59 treated like source, but is wrapped as a subroutine, and passed to
60 B::Concise::compile().
62 $subref = eval "sub{$src}";
64 I suppose I should also explain these more, but they seem obvious.
66 # prog => 'sort @a', # run in subprocess, aka -MO=Concise
67 # noanchors => 1, # no /^$/. needed for 1-liners like above
69 # skip => 1, # skips test
70 # todo => 'excuse', # anticipated failures
71 # fail => 1 # fails (by redirecting result)
72 # debug => 1, # turns on regex debug for match test !!
73 # retry => 1 # retry with debug on test failure
75 =head1 Test Philosophy
77 2 platforms --> 2 reftexts: You want an accurate test, independent of
78 which platform you're on. So, two refdata properties, 'expect' and
79 'expect_nt', carry renderings taken from threaded and non-threaded
80 builds. This has several benefits:
82 1. native reference data allows closer matching by regex.
83 2. samples can be eyeballed to grok t-nt differences.
84 3. data can help to validate mkCheckRex() operation.
85 4. can develop regexes which accomodate t-nt differences.
86 5. can test with both native and cross+converted regexes.
88 Cross-testing (expect_nt on threaded, expect on non-threaded) exposes
89 differences in B::Concise output, so mkCheckRex has code to do some
90 cross-test manipulations. This area needs more work.
94 One consequence of a single-function API is difficulty controlling
95 test-mode. Ive chosen for now to use a package hash, %gOpts, to store
96 test-state. These properties alter checkOptree() function, either
97 short-circuiting to selftest, or running a loop that runs the testcase
98 2^N times, varying conditions each time. (current N is 2 only).
100 So Test-mode is controlled with cmdline args, also called options below.
101 Run with 'help' to see the test-state, and how to change it.
105 This argument invokes runSelftest(), which tests a regex against the
106 reference renderings that they're made from. Failure of a regex match
107 its 'mold' is a strong indicator that mkCheckRex is buggy.
109 That said, selftest mode currently runs a cross-test too, they're not
110 completely orthogonal yet. See below.
112 =head2 testmode=cross
114 Cross-testing is purposely creating a T-NT mismatch, looking at the
115 fallout, and tweaking the regex to deal with it. Thus tests lead to
116 'provably' complete understanding of the differences.
118 The tweaking appears contrary to the 2-refs philosophy, but the tweaks
119 will be made in conversion-specific code, which (will) handles T->NT
120 and NT->T separately. The tweaking is incomplete.
122 A reasonable 1st step is to add tags to indicate when TonNT or NTonT
123 is known to fail. This needs an option to force failure, so the
124 test.pl reporting mechanics show results to aid the user.
126 =head2 testmode=native
128 This is normal mode. Other valid values are: native, cross, both.
130 =head2 checkOptree Notes
132 Accepts test code, renders its optree using B::Concise, and matches that
133 rendering against a regex built from one of 2 reference-renderings %in data.
135 The regex is built by mkCheckRex(\%in), which scrubs %in data to
136 remove match-irrelevancies, such as (args) and [args]. For example,
137 it strips leading '# ', making it easy to cut-paste new tests into
138 your test-file, run it, and cut-paste actual results into place. You
139 then retest and reedit until all 'errors' are gone. (now make sure you
140 haven't 'enshrined' a bug).
142 name: The test name. May be augmented by a label, which is built from
143 important params, and which helps keep names in sync with whats being
150 use B::Concise qw(walk_output);
152 $Data::Dumper::Sortkeys = 1;
155 $SIG{__WARN__} = sub {
157 $err =~ m/Subroutine re::(un)?install redefined/ and return;
161 # but wait - more skullduggery !
162 sub OptreeCheck::import { &getCmdLine; } # process @ARGV
164 # %gOpts params comprise a global test-state. Initial values here are
165 # HELP strings, they MUST BE REPLACED by runtime values before use, as
166 # is done by getCmdLine(), via import
168 our %gOpts = # values are replaced at runtime !!
170 # scalar values are help string
171 rextract => 'writes src-code todo same Optree matching',
172 vbasic => 'prints $str and $rex',
173 retry => 'retry failures after turning on re debug',
174 retrydbg => 'retry failures after turning on re debug',
175 selftest => 'self-tests mkCheckRex vs the reference rendering',
176 selfdbg => 'redo failing selftests with re debug',
177 xtest => 'extended thread/non-thread testing',
178 fail => 'force all test to fail, print to stdout',
179 dump => 'dump cmdline arg prcessing',
180 rexpedant => 'try tighter regex, still buggy',
181 noanchors => 'dont anchor match rex',
182 help => 0, # 1 ends in die
184 # array values are one-of selections, with 1st value as default
185 # tbc: 1st value is help, 2nd is default
186 testmode => [qw/ native cross both /],
190 our $threaded = 1 if $Config::Config{usethreads};
191 our $platform = ($threaded) ? "threaded" : "plain";
192 our $thrstat = ($threaded) ? "threaded" : "nonthreaded";
194 our ($MatchRetry,$MatchRetryDebug); # let mylike be generic
196 *MatchRetry = \$gOpts{retry}; # but alias it into %gOpts
197 *MatchRetryDebug = \$gOpts{retrydbg}; # but alias it into %gOpts
200 both => [ 'expect', 'expect_nt'],
201 native => [ ($threaded) ? 'expect' : 'expect_nt'],
202 cross => [ !($threaded) ? 'expect' : 'expect_nt'],
203 expect => [ 'expect' ],
204 expect_nt => [ 'expect_nt' ],
207 our %msgs # announce cross-testing.
210 'expect_nt-threaded' => " (Non-threaded-ref on Threaded-build)",
211 'expect-nonthreaded' => " (Threaded-ref on Non-threaded-build)",
212 # native - nothing to say
213 'expect_nt-nonthreaded' => '',
214 'expect-threaded' => '',
218 sub getCmdLine { # import assistant
220 print(qq{\n$0 accepts args to update these state-vars:
221 turn on a flag by typing its name,
222 select a value from list by typing name=val.\n },
224 if grep /help/, @ARGV;
226 # replace values for each key !! MUST MARK UP %gOpts
227 foreach my $opt (keys %gOpts) {
229 # scan ARGV for known params
230 if (ref $gOpts{$opt} eq 'ARRAY') {
232 # $opt is a One-Of construct
233 # replace with valid selection from the list
235 # uhh this WORKS. but it's inscrutable
236 # grep s/$opt=(\w+)/grep {$_ eq $1} @ARGV and $gOpts{$opt}=$1/e, @ARGV;
238 if (grep s/$opt=(\w+)/$tval=$1/e, @ARGV) {
239 # check val before accepting
240 my @allowed = @{$gOpts{$opt}};
241 if (grep { $_ eq $tval } @allowed) {
242 $gOpts{$opt} = $tval;
244 else {die "invalid value: '$tval' for $opt\n"}
247 # take 1st val as default
248 $gOpts{$opt} = ${$gOpts{$opt}}[0]
249 if ref $gOpts{$opt} eq 'ARRAY';
251 else { # handle scalars
253 # if 'opt' is present, true
254 $gOpts{$opt} = (grep /$opt/, @ARGV) ? 1 : 0;
256 # override with 'foo' if 'opt=foo' appears
257 grep s/$opt=(.*)/$gOpts{$opt}=$1/e, @ARGV;
260 print("$0 heres current state:\n", Dumper \%gOpts)
261 if $gOpts{help} or $gOpts{dump};
263 exit if $gOpts{help};
266 ##################################
271 my ($in, $res) = (\%in,0); # set up privates.
273 print "checkOptree args: ",Dumper \%in if $in{dump};
276 skip($in{name}, 1) if $in{skip};
277 return runSelftest(\%in) if $gOpts{selftest};
279 my $rendering = getRendering(\%in); # get the actual output
280 fail("FORCED: $in{name}:\n$rendering") if $gOpts{fail}; # silly ?
282 # Test rendering against ..
284 foreach $want (@{$modes{$gOpts{testmode}}}) {
285 local $TODO = $in{todo} if $in{todo};
287 my ($rex,$txt,$rexstr) = mkCheckRex(\%in,$want);
288 my $cross = $msgs{"$want-$thrstat"};
290 # bad is anticipated failure on cross testing ONLY
291 my $bad = (0 or ( $cross && $in{crossfail})
292 or (!$cross && $in{fail})
293 or 0); # no undefs! pedant
295 # couldn't bear to pass \%in to likeyn
296 $res = mylike ( # custom test mode stuff
298 $in{retry} || $gOpts{retry},
299 $in{debug} || $gOpts{retrydbg},
302 # remaining is std API
303 $rendering, qr/$rex/ms, "$cross $in{name} $in{label}")
305 printhelp(\%in, $rendering, $rex);
315 # may help get/keep test output consistent
317 return if $in->{name};
319 my $buf = (ref $in->{bcopts})
320 ? join(',', @{$in->{bcopts}}) : $in->{bcopts};
322 foreach (qw( note prog code )) {
323 $buf .= " $_: $in->{$_}" if $in->{$_} and not ref $in->{$_};
325 return $in->{label} = $buf;
329 # generate a set of test-cases from the options
332 foreach $want (@{$modes{$gOpts{testmode}}}) {
339 # tests the test-cases offered (expect, expect_nt)
340 # needs Unification with above.
343 foreach $want (@{$modes{$gOpts{testmode}}}) {}
345 for my $provenance (qw/ expect expect_nt /) {
346 next unless $in->{$provenance};
347 my ($rex,$gospel) = mkCheckRex($in, $provenance);
348 return unless $gospel;
350 my $cross = $msgs{"$provenance-$thrstat"};
351 my $bad = (0 or ( $cross && $in->{crossfail})
352 or (!$cross && $in->{fail})
354 # couldn't bear to pass \%in to likeyn
355 $res = mylike ( [ !$bad,
356 $in->{retry} || $gOpts{retry},
357 $in->{debug} || $gOpts{retrydbg},
360 $rendering, qr/$rex/ms, "$cross $in{name}")
368 # note dependence on unlike()
369 my ($control) = shift;
370 my ($yes,$retry,$debug,$postmortem) = @$control; # or dies
371 my ($got, $expected, $name, @mess) = @_; # pass thru mostly
373 die "unintended usage, expecting Regex". Dumper \@_
374 unless ref $_[1] eq 'Regexp';
376 #ok($got=~/$expected/, "wow");
378 # same as A ^ B, but B has side effects
379 my $ok = ( (!$yes and unlike($got, $expected, $name, @mess))
380 or ($yes and like($got, $expected, $name, @mess)));
382 if (not $ok and $postmortem) {
383 # split rexstr into units that should eat leading lines.
384 my @rexs = map qr/^$_/, split (/\n/,$postmortem);
385 foreach my $rex (@rexs) {
386 #$got =~ s/($rex)/ate: $1/msg; # noisy
387 $got =~ s/($rex)\n//msg; # remove matches
389 print "sequentially deconstructed, these are unmatched:\n$got\n";
392 if (not $ok and $retry) {
393 # redo, perhaps with use re debug - NOT ROBUST
394 eval "use re 'debug'" if $debug;
395 $ok = (!$yes and unlike($got, $expected, "(RETRY) $name", @mess)
396 or $yes and like($got, $expected, "(RETRY) $name", @mess));
405 die "getRendering: code or prog is required\n"
406 unless $in->{code} or $in->{prog};
408 my @opts = get_bcopts($in);
409 my $rendering = ''; # suppress "Use of uninitialized value in open"
412 $rendering = runperl( switches => ['-w',join(',',"-MO=Concise",@opts)],
413 prog => $in->{prog}, stderr => 1,
416 my $code = $in->{code};
417 unless (ref $code eq 'CODE') {
418 # treat as source, and wrap
419 $code = eval "sub { $code }";
420 die "$@ evaling code 'sub { $in->{code} }'\n"
421 unless ref $code eq 'CODE';
423 # set walk-output b4 compiling, which writes 'announce' line
424 walk_output(\$rendering);
426 fail("forced failure: stdout follows");
427 walk_output(\*STDOUT);
429 my $opwalker = B::Concise::compile(@opts, $code);
430 die "bad BC::compile retval" unless ref $opwalker eq 'CODE';
432 B::Concise::reset_sequence();
439 # collect concise passthru-options if any
443 @opts = (ref $in->{bcopts} eq 'ARRAY')
444 ? @{$in->{bcopts}} : ($in->{bcopts});
449 # needless complexity due to 'too much info' from B::Concise v.60
450 my $announce = 'B::Concise::compile\(CODE\(0x[0-9a-f]+\)\)';;
453 # converts expected text into Regexp which should match against
454 # unaltered version. also adjusts threaded => non-threaded
455 my ($in, $want) = @_;
456 eval "no re 'debug'";
458 my $str = $in->{expect} || $in->{expect_nt}; # standard bias
459 $str = $in->{$want} if $want; # stated pref
461 die "no reftext found for $want: $in->{name}" unless $str;
462 #fail("rex-str is empty, won't allow false positives") unless $str;
464 $str =~ s/^\# //mg; # ease cut-paste testcase authoring
465 my $reftxt = $str; # extra return val !!
467 # convert all (args) and [args] to temp forms wo bracing
468 $str =~ s/\[(.*?)\]/__CAPSQR$1__/msg;
469 $str =~ s/\((.*?)\)/__CAPRND$1__/msg;
470 $str =~ s/\((.*?)\)/__CAPRND$1__/msg; # nested () in nextstate
472 # escape bracing, etc.. manual \Q (doesnt escape '+')
473 $str =~ s/([\[\]()*.\$\@\#\|{}])/\\$1/msg;
475 # now replace temp forms with original, preserving reference bracing
476 $str =~ s/__CAPSQR(.*?)__\b/\\[$1\\]/msg; # \b is important
477 $str =~ s/__CAPRND(.*?)__\b/\\($1\\)/msg;
478 $str =~ s/__CAPRND(.*?)__\b/\\($1\\)/msg; # nested () in nextstate
480 # no 'invisible' failures in debugger
481 $str =~ s/(?:next|db)state(\\\(.*?\\\))/(?:next|db)state(.*?)/msg;
484 $str =~ s/:-?\d+,-?\d+/:-?\\d+,-?\\d+/msg; # FAKE line numbers
485 $str =~ s/match\\\(.*?\\\)/match\(.*?\)/msg; # match args
486 $str =~ s/(0x[0-9A-Fa-f]+)/0x[0-9A-Fa-f]+/msg; # hexnum values
487 $str =~ s/".*?"/".*?"/msg; # quoted strings
489 croak "no reftext found for $want: $in->{name}"
490 unless $str =~ /\w+/; # fail unless a real test
492 # $str = '.*' if 1; # sanity test
493 # $str .= 'FAIL' if 1; # sanity test
495 # allow -eval, banner at beginning of anchored matches
496 $str = "(-e .*?)?(B::Concise::compile.*?)?\n" . $str
497 unless $in->{noanchors};
499 eval "use re 'debug'" if $debug;
500 my $qr = ($in->{noanchors}) ? qr/$str/ms : qr/^$str$/ms ;
503 return ($qr, $reftxt, $str) if wantarray;
509 # crufty - may be still useful
510 my ($in, $rendering, $rex) = @_;
511 print "<$rendering>\nVS\n<$rex>\n" if $gOpts{vbasic};
513 # save this output to afile, edit out 'ok's and 1..N
514 # then perl -d afile, and add re 'debug' to suit.
515 print("\$str = q%$rendering%;\n".
516 "\$rex = qr%$rex%;\n\n".
517 #"print \"\$str =~ m%\$rex%ms \";\n".
518 "\$str =~ m{\$rex}ms or print \"doh\\n\";\n\n")
519 if $in{rextract} or $gOpts{rextract};
523 #########################
524 # support for test writing
527 my $testct = shift || 1;
533 \@INC = qw(../lib ../ext/B/t);
534 require q(./test.pl);
537 plan tests => $testct;
543 sub OptreeCheck::wrap {
545 $code =~ s/(?:(\#.*?)\n)//gsm;
546 $code =~ s/\s+/ /mgs;
548 return unless $code =~ /\S/;
553 checkOptree(note => q{$comment},
556 expect => <<EOT_EOT, expect_nt => <<EONT_EONT);
566 sub OptreeCheck::gentest {
567 my ($code,$opts) = @_;
568 my $rendering = getRendering({code => $code});
569 my $testcode = OptreeCheck::wrap($code);
570 return unless $testcode;
572 # run the prog, capture 'reference' concise output
573 my $preamble = preamble(1);
574 my $got = runperl( prog => "$preamble $testcode", stderr => 1,
575 #switches => ["-I../ext/B/t", "-MOptreeCheck"],
578 # extract the 'reftext' ie the got 'block'
579 if ($got =~ m/got \'.*?\n(.*)\n\# \'\n\# expected/s) {
581 #and plug it into the test-src
583 $testcode =~ s/ThreadedRef/$reftext/;
585 $testcode =~ s/NonThreadRef/$reftext/;
587 my $b4 = q{expect => <<EOT_EOT, expect_nt => <<EONT_EONT};
588 my $af = q{expect => <<'EOT_EOT', expect_nt => <<'EONT_EONT'};
589 $testcode =~ s/$b4/$af/;
592 if ($internal_retest) {
593 $got = runperl( prog => "$preamble $testcode", stderr => 1,
594 #switches => ["-I../ext/B/t", "-MOptreeCheck"],
604 sub OptreeCheck::processExamples {
606 # gets array of paragraphs, which should be tests.
608 foreach my $file (@files) {
609 open (my $fh, $file) or die "cant open $file: $!\n";
612 print preamble (scalar @chunks);
613 foreach $t (@chunks) {
614 print "\n\n=for gentest\n\n# chunk: $t=cut\n\n";
615 print OptreeCheck::gentest ($t);
620 # OK - now for the final insult to your good taste...
622 if ($0 =~ /OptreeCheck\.pm/) {
625 require './t/test.pl';
627 # invoked as program. Work like former gentest.pl,
628 # ie read files given as cmdline args,
629 # convert them to usable test files.
632 Getopt::Std::getopts('') or
633 die qq{ $0 sample-files* # no options
635 expecting filenames as args. Each should have paragraphs,
636 these are converted to checkOptree() tests, and printed to
637 stdout. Redirect to file then edit for test. \n};
639 OptreeCheck::processExamples(@ARGV);
648 mkCheckRex receives the full testcase object, and constructs a regex.
649 1st, it selects a reftxt from either the expect or expect_nt items.
651 Once selected, reftext is massaged & converted into a Regex that
652 accepts 'good' concise renderings, with appropriate input variations,
653 but is otherwise as strict as possible. For example, it should *not*
654 match when opcode flags change, or when optimizations convert an op to
657 =head2 match criteria
659 Opcode arguments (text within braces) are disregarded for matching
660 purposes. This loses some info in 'add[t5]', but greatly simplifys
661 matching 'nextstate(main 22 (eval 10):1)'. Besides, we are testing
662 for regressions, not for complete accuracy.
664 The regex is anchored by default, but can be suppressed with
665 'noanchors', allowing 1-liner tests to succeed if opcode is found.
667 =head1 TEST DEVELOPMENT SUPPORT
669 This optree regression testing framework needs tests in order to find
670 bugs. To that end, OptreeCheck has support for developing new tests,
671 according to the following model:
673 1. write a set of sample code into a single file, one per
674 paragraph. f_map and f_sort in ext/B/t/ are examples.
676 2. run OptreeCheck as a program on the file
678 ./perl -Ilib ext/B/t/OptreeCheck.pm -w ext/B/t/f_map
679 ./perl -Ilib ext/B/t/OptreeCheck.pm -w ext/B/t/f_sort
681 gentest reads the sample code, runs each to generate a reference
682 rendering, folds this rendering into an optreeCheck() statement,
683 and prints it to stdout.
685 3. run the output file as above, redirect to files, then rerun on
686 same build (for sanity check), and on thread-opposite build. With
687 editor in 1 window, and cmd in other, it's fairly easy to cut-paste
688 the gots into the expects, easier than running step 2 on both
689 builds then trying to sdiff them together.
693 There's a considerable amount of cruft in the whole arg-handling setup.
694 I'll replace / strip it before 5.10
696 Treat %in as a test object, interwork better with Test::*
698 Refactor mkCheckRex() and selfTest() to isolate the selftest,
699 crosstest, etc selection mechanics.
701 improve retry, retrydbg, esp. it's control of eval "use re debug".
702 This seems to work part of the time, but isn't stable enough.