1package My::Module::Test;
2
3use strict;
4use warnings;
5
6use Exporter;
7
8our @ISA = ( qw{ Exporter } );
9
10use PPIx::Regexp;
11use PPIx::Regexp::Dumper;
12use PPIx::Regexp::Element;
13use PPIx::Regexp::Tokenizer;
14use PPIx::Regexp::Util qw{ __choose_tokenizer_class __instance };
15use Scalar::Util qw{ looks_like_number refaddr };
16use Test::More 0.88;
17
18our $VERSION = '0.082';
19
20use constant ARRAY_REF	=> ref [];
21
22our @EXPORT_OK = qw{
23    builder
24    cache_count
25    choose
26    klass
27    cmp_ok
28    content
29    count
30    diag
31    different
32    done_testing
33    dump_result
34    error
35    fail
36    false
37    finis
38    equals
39    is
40    navigate
41    note
42    ok
43    parse
44    pass
45    plan
46    ppi
47    result
48    replace_characters
49    skip
50    tokenize
51    true
52    value
53};
54
55our @EXPORT = @EXPORT_OK;	## no critic (ProhibitAutomaticExportation)
56
57push @EXPORT_OK, qw{ __quote };
58
59my (
60    $initial_class,	# For static methods; set by parse() or tokenize()
61    $kind,		# of thing; set by parse() or tokenize()
62    $nav,		# Navigation used to get to current object, as a
63			#    string.
64    $obj,		# Current object:
65    			#    PPIx::Regexp::Tokenizer if set by tokenize(),
66			#    PPIx::Regexp if set by parse(), or
67			#    PPIx::Regexp::Element if set by navigate().
68    $parse,		# Result of parse:
69    			#    array ref if set by tokenize(), or
70			#    PPIx::Regexp object if set by parse()
71    %replace_characters, # Troublesome characters replaced in output
72			# before testing
73    $result,		# Operation result.
74);
75
76sub builder {
77    return Test::More->builder();
78}
79
80sub cache_count {
81    my ( $expect ) = @_;
82    defined $expect or $expect = 0;
83    $obj = undef;
84    $parse = undef;
85    _pause();
86    $result = PPIx::Regexp->__cache_size();
87    # cperl does not seem to like goto &xxx; it throws a deep recursion
88    # error if you do it enough times.
89    $Test::Builder::Level = $Test::Builder::Level + 1;
90    return is( $result, $expect,
91	"Should be $expect leftover cache contents" );
92}
93
94sub choose {
95    my @args = @_;
96    $obj = $parse;
97    return navigate( @args );
98}
99
100sub klass {
101    my ( $class ) = @_;
102    $result = ref $obj || $obj;
103    # cperl does not seem to like goto &xxx; it throws a deep recursion
104    # error if you do it enough times.
105    $Test::Builder::Level = $Test::Builder::Level + 1;
106    if ( defined $class ) {
107	return isa_ok( $obj, $class, "$kind $nav" );
108    } else {
109	return is( ref $obj || undef, $class, "Class of $kind $nav" );
110    }
111}
112
113sub content {		## no critic (RequireArgUnpacking)
114    # For some reason cperl seems to have no problem with this
115    unshift @_, 'content';
116    goto &_method_result;
117}
118
119sub count {
120    my ( @args ) = @_;
121    my $expect = pop @args;
122    # cperl does not seem to like goto &xxx; it throws a deep recursion
123    # error if you do it enough times.
124    $Test::Builder::Level = $Test::Builder::Level + 1;
125    if ( ARRAY_REF eq ref $parse ) {
126	$result = @{ $parse };
127	return is( $result, $expect, "Expect $expect tokens" );
128    } elsif ( ARRAY_REF eq ref $obj ) {
129	$result = @{ $obj };
130	return is( $result, $expect, "Expect $expect tokens" );
131    } elsif ( $obj->can( 'children' ) ) {
132	$result = $obj->children();
133	return is( $result, $expect, "Expect $expect children" );
134    } else {
135	$result = $obj->can( 'children' );
136	return ok( $result, ref( $obj ) . "->can( 'children')" );
137    }
138}
139
140sub different {
141    my @args = @_;
142    @args < 3 and unshift @args, $obj;
143    my ( $left, $right, $name ) = @args;
144    # cperl does not seem to like goto &xxx; it throws a deep recursion
145    # error if you do it enough times.
146    $Test::Builder::Level = $Test::Builder::Level + 1;
147    if ( ! defined $left && ! defined $right ) {
148	return ok( undef, $name );
149    } elsif ( ! defined $left || ! defined $right ) {
150	return ok( 1, $name );
151    } elsif ( ref $left && ref $right ) {
152	return ok( refaddr( $left ) != refaddr( $right ), $name );
153    } elsif ( ref $left || ref $right ) {
154	return ok( 1, $name );
155    } elsif ( looks_like_number( $left ) && looks_like_number( $right ) ) {
156	return ok( $left != $right, $name );
157    } else {
158	return ok( $left ne $right, $name );
159    }
160}
161
162sub dump_result {
163    my ( $opt, @args ) = _parse_constructor_args( { test => 1 }, @_ );
164    if ( $opt->{test} ) {
165	my ( $expect, $name ) = splice @args, -2;
166	my $got = PPIx::Regexp::Dumper->new( $obj, @args )->string();
167	# cperl does not seem to like goto &xxx; it throws a deep
168	# recursion error if you do it enough times.
169	$Test::Builder::Level = $Test::Builder::Level + 1;
170	return is( $got, $expect, $name );
171    } elsif ( __instance( $result, 'PPIx::Regexp::Tokenizer' ) ||
172	__instance( $result, 'PPIx::Regexp::Element' ) ) {
173	diag( PPIx::Regexp::Dumper->new( $obj, @args )->string() );
174    } elsif ( eval { require YAML; 1; } ) {
175	diag( "Result dump:\n", YAML::Dump( $result ) );
176    } elsif ( eval { require Data::Dumper; 1 } ) {
177	diag( "Result dump:\n", Data::Dumper::Dumper( $result ) );
178    } else {
179	diag( "Result dump unavailable.\n" );
180    }
181    return;
182}
183
184sub equals {
185    my @args = @_;
186    @args < 3 and unshift @args, $obj;
187    my ( $left, $right, $name ) = @args;
188    # cperl does not seem to like goto &xxx; it throws a deep recursion
189    # error if you do it enough times.
190    $Test::Builder::Level = $Test::Builder::Level + 1;
191    if ( ! defined $left && ! defined $right ) {
192	return ok( 1, $name );
193    } elsif ( ! defined $left || ! defined $right ) {
194	return ok( undef, $name );
195    } elsif ( ref $left && ref $right ) {
196	return ok( refaddr( $left ) == refaddr( $right ), $name );
197    } elsif ( ref $left || ref $right ) {
198	return ok( undef, $name );
199    } elsif ( looks_like_number( $left ) && looks_like_number( $right ) ) {
200	return ok( $left == $right, $name );
201    } else {
202	return ok( $left eq $right, $name );
203    }
204}
205
206sub error {		## no critic (RequireArgUnpacking)
207    unshift @_, 'error';
208    goto &_method_result;
209}
210
211sub false {
212    my ( $method, $args ) = @_;
213    ARRAY_REF eq ref $args
214	or $args = [ $args ];
215    my $class = ref $obj;
216    # cperl does not seem to like goto &xxx; it throws a deep recursion
217    # error if you do it enough times.
218    $Test::Builder::Level = $Test::Builder::Level + 1;
219    if ( $obj->can( $method ) ) {
220	$result = $obj->$method( @{ $args } );
221	my $fmtd = _format_args( $args );
222	return ok( ! $result, "$class->$method$fmtd is false" );
223    } else {
224	$result = undef;
225	return ok( undef, "$class->$method() exists" );
226    }
227}
228
229sub finis {
230    $obj = $parse = $result = undef;
231    _pause();
232    $result = PPIx::Regexp::Element->__parent_keys();
233    # cperl does not seem to like goto &xxx; it throws a deep recursion
234    # error if you do it enough times.
235    $Test::Builder::Level = $Test::Builder::Level + 1;
236    return is( $result, 0, 'Should be no leftover objects' );
237}
238
239{
240
241    my %array = map { $_ => 1 } qw{
242	children delimiters finish schildren start tokens type
243    };
244
245    sub navigate {
246	my @args = @_;
247	my $scalar = 1;
248	@args > 1
249	    and ARRAY_REF eq ref $args[-1]
250	    and @{ $args[-1] } == 0
251	    and $array{$args[-2]}
252	    and $scalar = 0;
253	my @nav = ();
254	while ( @args ) {
255	    if ( __instance( $args[0], 'PPIx::Regexp::Element' ) ) {
256		$obj = shift @args;
257	    } elsif ( ARRAY_REF eq ref $obj ) {
258		my $inx = shift @args;
259		push @nav, $inx;
260		$obj = $obj->[$inx];
261	    } else {
262		my $method = shift @args;
263		my $args = shift @args;
264		ARRAY_REF eq ref $args
265		    or $args = [ $args ];
266		push @nav, $method, $args;
267		$obj->can( $method ) or return;
268		if ( @args || $scalar ) {
269		    $obj = $obj->$method( @{ $args } ) or return;
270		} else {
271		    $obj = [ $obj->$method( @{ $args } ) ];
272		}
273	    }
274	}
275	$nav = __quote( @nav );
276	$nav =~ s/ ' ( \w+ ) ' , /$1 =>/smxg;
277	$nav =~ s/ \[ \s+ \] /[]/smxg;
278	$result = $obj;
279	return $obj;
280    }
281
282}
283
284sub parse {		## no critic (RequireArgUnpacking)
285    my ( $opt, $regexp, @args ) = _parse_constructor_args(
286	{ test => 1 }, @_ );
287    $initial_class = 'PPIx::Regexp';
288    $kind = 'element';
289    $result = $obj = $parse = PPIx::Regexp->new( $regexp, @args );
290    $nav = '';
291    $opt->{test} or return;
292    # cperl does not seem to like goto &xxx; it throws a deep recursion
293    # error if you do it enough times.
294    $Test::Builder::Level = $Test::Builder::Level + 1;
295    return isa_ok( $parse, 'PPIx::Regexp',
296	_replace_characters( $regexp ) );
297}
298
299sub ppi {		## no critic (RequireArgUnpacking)
300    my @args = @_;
301    my $expect = pop @args;
302    $result = undef;
303    defined $obj and $result = $obj->ppi()->content();
304    my $safe;
305    if ( defined $result ) {
306	($safe = $result) =~ s/([\\'])/\\$1/smxg;
307    } else {
308	$safe = 'undef';
309    }
310    # cperl does not seem to like goto &xxx; it throws a deep recursion
311    # error if you do it enough times.
312    $Test::Builder::Level = $Test::Builder::Level + 1;
313    return is( $result, $expect, "$kind $nav ppi() content '$safe'" );
314}
315
316sub replace_characters {
317    %replace_characters	= @_;
318    return;
319}
320
321sub result {
322    return $result;
323}
324
325sub tokenize {		## no critic (RequireArgUnpacking)
326    my ( $opt, $regexp, @args ) = _parse_constructor_args(
327	{ test => 1, tokens => 1 }, @_ );
328    my %args = @args;
329    $initial_class = __choose_tokenizer_class( $regexp, \%args );
330    $kind = 'token';
331    $obj = $initial_class->new( $regexp, @args );
332    if ( $obj && $opt->{tokens} ) {
333	$parse = [ $obj->tokens() ];
334    } else {
335	$parse = [];
336    }
337    $result = $parse;
338    $nav = '';
339    $opt->{test} or return;
340    $Test::Builder::Level = $Test::Builder::Level + 1;
341    return isa_ok( $obj, 'PPIx::Regexp::Tokenizer',
342	_replace_characters( $regexp ) );
343}
344
345sub true {		## no critic (RequireArgUnpacking)
346    my ( $method, $args ) = @_;
347    ARRAY_REF eq ref $args
348	or $args = [ $args ];
349    my $class = ref $obj;
350    # cperl does not seem to like goto &xxx; it throws a deep recursion
351    # error if you do it enough times.
352    $Test::Builder::Level = $Test::Builder::Level + 1;
353    if ( $obj->can( $method ) ) {
354	$result = $obj->$method( @{ $args } );
355	my $fmtd = _format_args( $args );
356	return ok( $result, "$class->$method$fmtd is true" );
357    } else {
358	$result = undef;
359	return ok( undef, "$class->$method() exists" );
360    }
361}
362
363sub value {		## no critic (RequireArgUnpacking)
364    my ( $method, $args, $expect ) = @_;
365    ARRAY_REF eq ref $args
366	or $args = [ $args ];
367
368    my $invocant = $obj || $initial_class;
369    my $class = ref $obj || $obj || $initial_class;
370    # cperl does not seem to like goto &xxx; it throws a deep recursion
371    # error if you do it enough times.
372    $Test::Builder::Level = $Test::Builder::Level + 1;
373    if ( ! $invocant->can( $method ) ) {
374	return ok( undef, "$class->$method() exists" );
375    }
376
377    $result = ARRAY_REF eq ref $expect ?
378	[ $invocant->$method( @{ $args } ) ] :
379	$invocant->$method( @{ $args } );
380
381    my $fmtd = _format_args( $args );
382    my $answer = _format_args( [ $expect ], bare => 1 );
383    if ( ref $result ) {
384	return is_deeply( $result, $expect,
385	    "${class}->$method$fmtd is $answer" );
386    } else {
387	return is( $result, $expect, "${class}->$method$fmtd is $answer" );
388    }
389}
390
391sub _format_args {
392    my ( $args, %opt ) = @_;
393    my @rslt;
394    foreach my $arg ( @{ $args } ) {
395	if ( ! defined $arg ) {
396	    push @rslt, 'undef';
397	} elsif ( looks_like_number( $arg ) ) {
398	    push @rslt, $arg;
399	} else {
400	    push @rslt, $arg;
401	    $rslt[-1] =~ s/ ' /\\'/smxg;
402	    $rslt[-1] = "'$rslt[-1]'";
403	}
404    }
405    my $string = join ', ', @rslt;
406    $opt{bare} and return $string;
407    @rslt or return '()';
408    return "( $string )";
409}
410
411sub _method_result {		## no critic (RequireArgUnpacking)
412    my ( $method, @args ) = @_;
413    my $expect = pop @args;
414    $result = undef;
415    defined $obj and $result = $obj->$method();
416    my $safe;
417    if ( defined $result ) {
418	($safe = $result) =~ s/([\\'])/\\$1/smxg;
419	$safe = "'$safe'";
420    } else {
421	$safe = 'undef';
422    }
423    @_ = _replace_characters( $result, $expect, "$kind $nav $method $safe" );
424    goto &is;
425}
426
427sub _parse_constructor_args {
428    my ( $opt, @args ) = @_;
429    my @rslt = ( $opt );
430    foreach my $arg ( @args ) {
431	if ( $arg =~ m/ \A - -? (no)? (\w+) \z /smx &&
432	    exists $opt->{$2} ) {
433	    $opt->{$2} = !$1;
434	} else {
435	    push @rslt, $arg;
436	}
437    }
438    return @rslt;
439}
440
441sub _pause {
442    if ( eval { require Time::HiRes; 1 } ) {	# Cargo cult programming.
443	Time::HiRes::sleep( 0.1 );		# Something like this is
444    } else {					# in PPI's
445	sleep 1;				# t/08_regression.t, and
446    }						# who am I to argue?
447    return;
448}
449
450# quote a string.
451sub __quote {
452    my @args = @_;
453    my @rslt;
454    foreach my $item ( @args ) {
455	if ( __instance( $item, 'PPIx::Regexp::Element' ) ) {
456	    $item = $item->content();
457	}
458	if ( ! defined $item ) {
459	    push @rslt, 'undef';
460	} elsif ( ARRAY_REF eq ref $item ) {
461	    push @rslt, join( ' ', '[', __quote( @{ $item } ), ']' );
462	} elsif ( looks_like_number( $item ) ) {
463	    push @rslt, $item;
464	} else {
465	    $item =~ s/ ( [\\'] ) /\\$1/smxg;
466	    push @rslt, "'$item'";
467	}
468    }
469    return join( ', ', @rslt );
470}
471
472sub _replace_characters {
473    my @arg = @_;
474    if ( keys %replace_characters ) {
475	foreach ( @arg ) {
476	    $_ = join '',
477	    # The following assumes I will never want to replace 0.
478	    map { $replace_characters{$_} || $_ }
479	    split qr<>;
480	}
481    }
482    wantarray
483	or return join '', @arg;
484    return @arg;
485}
486
4871;
488
489__END__
490
491=head1 NAME
492
493My::Module::Test - support for testing PPIx::Regexp
494
495=head1 SYNOPSIS
496
497 use lib qw{ inc };
498 use My::Module::Test;
499
500 parse   ( '/foo/' );
501 value   ( failures => [], 0 );
502 klass   ( 'PPIx::Regexp' );
503 choose  ( child => 0 );
504 klass   ( 'PPIx::Regexp::Token::Structure' );
505 content ( '' );
506 # and so on
507
508=head1 DETAILS
509
510This module is B<private> to the C<PPIx-Regexp> module. Its contents can
511be changed without warning. This was always the intent, and this
512paragraph should have been included in the POD much earlier than it
513actually was.
514
515This module exports various subroutines in support of testing
516C<PPIx::Regexp>. Most of these are tests, with C<Test::More> doing the
517dirty work. A few simply set up data for tests.
518
519The whole test rig works by parsing (or tokenizing) a regular
520expression, followed by a series of unit tests on the results of the
521parse. Each set of unit tests is performed by selecting an object to
522test using the C<choose> or C<navigate> subroutine, followed by the
523tests to be performed on that object. A few tests do not test parse
524objects, but rather the state of the system as a whole.
525
526The following subroutines are exported:
527
528=head2 builder
529
530This subroutine returns the underlying L<Test::Builder|Test::Builder>
531object.
532
533=head2 cache_count
534
535 cache_count( 1 );
536
537This test compares the number of objects in the C<new_from_cache> cache
538to its argument, succeeding if they are equal. If no argument is passed,
539the default is 0.
540
541=head2 choose
542
543 choose( 2 );  # For tokenizer
544 choose( child => 1, child => 2, type => 0 ); # For full parse
545
546This subroutine does not itself represent a test. It chooses an object
547from the parse tree for further testing. If testing a tokenizer, the
548argument is the token number (from 0) to select. If testing a full
549parse, the arguments are the navigation methods used to reach the
550object to be tested, starting from the C<PPIx::Regexp> object. The
551arguments to the methods are passed in an array reference, but if there
552is a single argument it can be passed as a scalar, as in the example.
553
554=head2 klass
555
556 klass( 'PPIx::Regexp::Token::Structure' );
557
558This test checks to see if the current object is of the given class, and
559succeeds if it is. If the current object is C<undef>, the test fails.
560
561This test was C<class>, but that tends to conflict with object systems.
562
563=head2 content
564
565 content( '\N{LATIN SMALL LETTER A}' );
566
567This test checks to see if the C<content> method of the current object
568is equal to the given string. If the current object is C<undef>, the
569test fails.
570
571=head2 cmp_ok
572
573This subroutine is exported from L<Test::More|Test::More>.
574
575=head2 count
576
577 count( 42 );
578
579This test checks the number of objects returned by an operation that
580returns more than one object. It succeeds if the number of objects
581returned is equal to the given number.
582
583This test is valid only after C<tokenize>, or a C<choose> or C<navigate>
584whose argument list ends in one of
585
586 children => []
587 finish => []
588 start => []
589 type => []
590
591=head2 different
592
593 different( $o1, $o2, 'Test name' );
594
595This test compares two things, succeeding if they are different.
596References are compared by reference address and scalars by value
597(numeric or string comparison as appropriate). If the first argument is
598omitted it defaults to the current object.
599
600=head2 dump_result
601
602 dump_result( tokens => 1, <<'EOD', 'Test tokenization dump' );
603 ... expected dump here ...
604 EOD
605
606This test performs the specified dump on the current object and succeeds
607if the result matches the expectation. The name of the test is the last
608argument, and the expected result is the next-to-last argument. All
609other arguments are passed to
610L<PPIx::Regexp::Dumper|PPIx::Regexp::Dumper>.
611
612Well, almost all other arguments are passed to the dumper. You can
613specify C<--notest> to skip the test. In this case the result of the
614last operation is dumped. L<PPIx::Regexp::Dumper|PPIx::Regexp::Dumper>
615is used if appropriate; otherwise you get a L<YAML|YAML> dump if that is
616available, or a L<Data::Dumper|Data::Dumper> dump if not. If no dumper
617class can be found, a diagnostic is produced. You can also specify
618C<--test>, but this is the default. This option is removed from the
619argument list before the test name (etc) is determined.
620
621=head2 equals
622
623 equals( $o1, $o2, 'Test name' );
624
625This test compares two things, succeeding if they are equal. References
626are compared by reference address and scalars by value (numeric or string
627comparison as appropriate). If the first argument is omitted it defaults
628to the current object.
629
630=head2 false
631
632 false( significant => [] );
633
634This test succeeds if the given method, with the given arguments, called
635on the current object, returns a false value.
636
637=head2 finis
638
639 finis();
640
641This test should be last in a series, and no references to parse objects
642should be held when it is run. It checks the number of objects in the
643internal C<%parent> hash, and succeeds if it is zero.
644
645=head2 navigate
646
647 navigate( snext_sibling => [] );
648
649Like C<choose>, this is not a test, but selects an object for testing.
650Unlike C<choose>, selection starts from the current object, not the top
651of the parse tree.
652
653=head2 parse
654
655 parse( 's/foo/bar/g' );
656
657This test parses the given regular expression into a C<PPIx::Regexp>
658object, and succeeds if a C<PPIx::Regexp> object was in fact generated.
659
660If you specify argument C<--notest>, the parse is done but no test is
661performed. You would do this if you expected the parse to fail (e.g. you
662are testing error handling). You can also explicitly specify C<--test>,
663but this is the default.
664
665All other arguments are passed to the L<PPIx::Regexp|PPIx::Regexp>
666constructor.
667
668=head2 plan
669
670This subroutine is exported from L<Test::More|Test::More>.
671
672=head2 content
673
674 ppi( '$foo' );
675
676This test calls the current object's C<ppi()> method, and checks to see
677if the content of the returned L<PPI::Document|PPI::Document> is equal
678to the given string. If the current object is C<undef> or does not have
679a C<ppi()> method, the test fails.
680
681=head2 result
682
683 my $val = result();
684
685This subroutine returns the result of the most recent operation that
686actually produces one. It should be called immediately after the
687operation, mostly because I have not documented all the subroutines that
688produce a result.
689
690=head2 tokenize
691
692 tokenize( 'm/foo/smx' );
693
694This test tokenizes the given regular expression into a
695C<PPIx::Regexp::Tokenizer> object, and succeeds if a
696C<PPIx::Regexp::Tokenizer> object was in fact generated.
697
698If you specify argument C<--notest>, the parse is done but no test is
699performed. You would do this if you expected the parse to fail (e.g. you
700are testing error handling). You can also explicitly specify C<--test>,
701but this is the default.
702
703If you specify argument C<--notokens>, the tokenizer is built, but the
704tokens are not extracted. You would do this when you want a subsequent
705operation to call C<tokens()>. You can also explicitly specify
706C<--tokens>, but this is the default.
707
708All other arguments are passed to the
709L<PPIx::Regexp::Tokenizer|PPIx::Regexp::Tokenizer> constructor.
710
711=head2 true
712
713 true( significant => [] );
714
715This test succeeds if the given method, with the given arguments, called
716on the current object, returns a true value.
717
718=head2 value
719
720 value( max_capture_number => [], 3 );
721
722This test succeeds if the given method, with the given arguments, called
723on the current object, returns the given value.
724
725If the current object is undefined, the given method is called on the
726intended initial class, otherwise there would be no way to test the
727errstr() method.
728
729The result of the method call is accessable via the L<result()|/result>
730subroutine.
731
732=head1 SUPPORT
733
734Support is by the author. Please file bug reports at
735L<https://rt.cpan.org/Public/Dist/Display.html?Name=PPIx-Regexp>,
736L<https://github.com/trwyant/perl-PPIx-Regexp/issues>, or in
737electronic mail to the author.
738
739=head1 AUTHOR
740
741Thomas R. Wyant, III F<wyant at cpan dot org>
742
743=head1 COPYRIGHT AND LICENSE
744
745Copyright (C) 2009-2021 by Thomas R. Wyant, III
746
747This program is free software; you can redistribute it and/or modify it
748under the same terms as Perl 5.10.0. For more details, see the full text
749of the licenses in the directory LICENSES.
750
751This program is distributed in the hope that it will be useful, but
752without any warranty; without even the implied warranty of
753merchantability or fitness for a particular purpose.
754
755=cut
756
757# ex: set textwidth=72 :
758