The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/perl

# Exhaustively test all possible Perl programs to a particular length

use strict;
use Carp 'croak';
BEGIN {
	no warnings 'once';
	$| = 1;
	$PPI::XS_DISABLE = 1;
	$PPI::Lexer::X_TOKENIZER ||= $ENV{X_TOKENIZER};
}

use vars qw{$MAX_CHARS $ITERATIONS $LENGTH @ALL_CHARS};
BEGIN {
	# When distributing, keep this in to verify the test script
	# is working correctly, but limit to 2 (maaaaybe 3) so we
	# don't slow the install process down too much.
	$MAX_CHARS  = 2;
	$ITERATIONS = 1000;
	$LENGTH     = 190;
	@ALL_CHARS  = (
		qw{a b c f g m q r s t w x y z V W X 0 1 8 9},
		';', '[', ']', '{', '}', '(', ')', '=', '?', '|', '+', '<',
		'>', '.', '!', '~', '^', '*', '$', '@', '&', ':', '%', ',',
		'\\', '/', '_', ' ', "\n", "\t", '-',
		 "'", '"', '`', '#', # Comment out to make parsing more intense
		);
	#my @ALL_CHARS = (
	#	qw{a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H
	#	I J K L M N O P Q R S T U V W X Y Z 0 1 2 3 4 5 6 7 8 9},
	#	';', '[', ']', '{', '}', '(', ')', '=', '?', '|', '+', '<', '>', '.',
	#	'!', '~', '^', '*', '$', '@', '&', ':', '%', '#', ',', "'", '"', '`',
	#	'\\', '/', '_', ' ', "\n", "\t", '-',
	#	);
}





#####################################################################
# Prepare

use Test::More tests => ($MAX_CHARS + $ITERATIONS + 3);
use Test::NoWarnings;
use File::Spec::Functions ':ALL';
use Params::Util qw{_INSTANCE};
use PPI;





#####################################################################
# Retest Previous Failures

test_code2( "( {8" );





#####################################################################
# Code/Dump Testing

my $failures   = 0;
my $last_index = scalar(@ALL_CHARS) - 1;
LENGTHLOOP:
foreach my $len ( 1 .. $MAX_CHARS ) {
	# Initialise the char array and failure count
	my $failures = 0;
	my @chars    = (0) x $len;

	# The main test loop
	CHARLOOP:
	while ( 1 ) {
		# Test the current set of chars
		my $code = join '', map { $ALL_CHARS[$_] } @chars;
		unless ( length($code) == $len ) {
			die "Failed sanity check. Error in the code generation mechanism";
		}
		test_code( $code );

		# Increment the last character
		$chars[$len - 1]++;

		# Cascade the wrapping as needed
		foreach ( reverse( 0 .. $len - 1 ) ) {
			next CHARLOOP unless $chars[$_] > $last_index;
			if ( $_ == 0 ) {
				# End of the iterations, move to the next length
				last CHARLOOP;
			}

			# Carry to the previous char
			$chars[$_] = 0;
			$chars[$_ - 1]++;
		}
	}
	is( $failures, 0, "No tokenizer failures for all $len-length programs" );
}





#####################################################################
# Test a series of random strings

my $count = 0;
foreach my $i ( 1 .. $ITERATIONS ) {
	# Generate a random string
	my $code = join( '',
		map { $ALL_CHARS[$_] }
		map { int(rand($last_index) + 1) }
		(1 .. $LENGTH)
		);

	# Test it as normal
	test_code2( $code );

	# Verify there are no stale %PARENT entries
	#my $quotable = quotable($code);
	#is( scalar(keys %PPI::Element::PARENT), 0,
	#	"%PARENT is clean \"$quotable\"" );
}

is( scalar(keys %PPI::Element::PARENT), 0,
	'No stale \%PARENT entries at the end of testing' );
%PPI::Element::PARENT = %PPI::Element::PARENT;





#####################################################################
# Support Functions

sub test_code2 {
	$failures    = 0;
	my $string   = shift;
	my $quotable = quotable($string);
	test_code( $string );
	is( $failures, 0, "String parses ok \"$quotable\"" );	
}

sub test_code {
	my $code      = shift;
	my $Document  = eval {
		# $SIG{__WARN__} = sub { croak('Triggered a warning') };
		PPI::Document->new(\$code);
	};

	# Version of the code for use in error messages
	my $quotable = quotable($code);
	unless ( _INSTANCE($Document, 'PPI::Document') ) {
		$failures++;
		diag( "\"$quotable\": Parser did not return a Document" );
		return;
	}
	my $joined          = $Document->serialize;
	my $joined_quotable = quotable($joined);
	unless ( $joined eq $code ) {
		$failures++;
		diag( "\"$quotable\": Document round-trips ok" );
		diag( "\"$joined_quotable\" (round-trips to)" );
		return;
	}
}

# Find the shortest failing substring of known bad string
sub quickcheck {
	my $code       = shift;
	my $fails      = $code;
	# $SIG{__WARN__} = sub { croak('Triggered a warning') };

	while ( length $fails ) {
		chop $code;
		my $Document = PPI::Document->new(\$code) or last;
		$fails = $code;
	}

	while ( length $fails ) {
		substr( $code, 0, 1, '' );
		my $Document = PPI::Document->new(\$code) or return $fails;
		$fails = $code;
	}

	return $fails;
}

sub quotable {
	my $quotable = shift;
	$quotable =~ s/\\/\\\\/g;
	$quotable =~ s/\t/\\t/g;
	$quotable =~ s/\n/\\n/g;
	$quotable =~ s/\$/\\\$/g;
	$quotable =~ s/\@/\\\@/g;
	$quotable =~ s/\"/\\\"/g;
	return $quotable;
}

exit(0);