File Coverage

blib/lib/Test/MixedScripts.pm
Criterion Covered Total %
statement 85 96 88.5
branch 20 26 76.9
condition 5 10 50.0
subroutine 19 24 79.1
pod 2 2 100.0
total 131 158 82.9


line stmt bran cond sub pod time code
1             package Test::MixedScripts;
2              
3 4     4   561145 use utf8;
  4         820  
  4         22  
4              
5             # ABSTRACT: test text for mixed and potentially confusable Unicode scripts
6              
7 4     4   199 use v5.16;
  4         15  
8 4     4   21 use warnings;
  4         24  
  4         203  
9              
10 4     4   21 use Carp qw( croak );
  4         9  
  4         327  
11 4     4   27 use Exporter 5.57 qw( import );
  4         130  
  4         188  
12 4     4   23 use File::Basename qw( basename );
  4         12  
  4         286  
13 4     4   43 use File::Spec;
  4         9  
  4         148  
14 4     4   1902 use IO qw( File );
  4         3939  
  4         26  
15 4     4   34977 use List::Util qw( first );
  4         7  
  4         264  
16 4     4   4254 use Unicode::UCD qw( charinfo charscripts );
  4         185750  
  4         451  
17              
18 4     4   32 use Test2::API 1.302200 qw( context );
  4         105  
  4         218  
19 4     4   2007 use Test2::Util::DistFiles v0.2.0 qw( manifest_files is_perl_file );
  4         68376  
  4         5258  
20              
21             our @EXPORT_OK = qw( all_perl_files_scripts_ok file_scripts_ok );
22              
23             our $VERSION = 'v0.6.3';
24              
25              
26             sub file_scripts_ok {
27 36     36 1 811151 my ( $file, @args ) = @_;
28              
29 36 100 100     238 my $options = @args == 1 && ref( $args[0] ) eq "HASH" ? $args[0] : { scripts => \@args };
30 36   50     113 $options->{scripts} //= [];
31 36 100       111 push @{ $options->{scripts} }, qw( Latin Common ) unless defined $options->{scripts}[0];
  5         28  
32              
33 36         111 my $ctx = context();
34              
35 36 100       11542 if ( my $error = _check_file_scripts( $file, $options ) ) {
36              
37 3         206 my ( $lino, $pre, $char ) = @{$error};
  3         11  
38              
39             # Ideally we would use charprop instead of charscript, since that supports Script_Extensions, but Unicode::UCD
40             # is not dual life and charprop is only available after v5.22.0.
41              
42 3         20 my $info = charinfo( ord($char) );
43             my $message = sprintf(
44             'Unexpected %s character %s on line %u character %u in %s',
45             $info->{script}, #
46 3   50     329325 $info->{name} || "NO NAME", #
47             $lino, #
48             length($pre) + 1, #
49             "$file"
50             );
51              
52 3         29 $ctx->fail( $file, $message );
53              
54             }
55             else {
56 32         138 $ctx->pass( $file );
57             }
58              
59 35         5998 $ctx->release;
60             }
61              
62             sub _check_file_scripts {
63 36     36   79 my ( $file, $options ) = @_;
64              
65 36         55 my @scripts = @{ $options->{scripts} };
  36         109  
66 36         88 my $default = _make_regex(@scripts);
67              
68 35 50       248 my $fh = IO::File->new( $file, "r" ) or croak "Cannot open ${file}: $!";
69              
70 35         4209 $fh->binmode(":utf8");
71              
72 35         375 my $current = $default;
73              
74 35         986 while ( my $line = $fh->getline ) {
75 1609         1931 my $re = $current;
76             # TODO custom comment prefix based on the file type
77 1609 100       3344 if ( $line =~ s/\s*##\s+Test::MixedScripts\s+(\w+(?:,\w+)*).*$// ) {
    100          
78 6         70 $re = _make_regex( split /,\s*/, $1 );
79             }
80             elsif ( $line =~ /^=for\s+Test::MixedScripts\s+(\w+(?:,\w+)*)$/ ) {
81 3 100       31 $current = $1 eq "default" ? $default : _make_regex( split /,\s*/, $1 );
82 3         26 next;
83             }
84              
85 1606 100       8533 unless ( $line =~ $re ) {
86 3         15 my $fail = _make_negative_regex(@scripts);
87 3         28 $line =~ $fail;
88 3         30 return [ $fh->input_line_number, ${^PREMATCH}, ${^MATCH} ];
89             }
90             }
91              
92 32         166 $fh->close;
93              
94 32         645 return 0;
95             }
96              
97             sub _make_regex_set {
98 47     47   83 state $scripts = { ASCII => undef, map { $_ => 1 } keys %{ charscripts() } };
  680         305354  
  4         25  
99 47 100   101   1351 if ( my $err = first { !exists $scripts->{$_} } @_ ) {
  101         287  
100 1         269 croak "Unknown script ${err}";
101             }
102 46 100       197 return join( "", map { $_ eq "ASCII" ? '\x00-\x7f' : sprintf( '\p{scx=%s}', $_ ) } @_ );
  100         405  
103             }
104              
105             sub _make_regex {
106 44     44   115 my $set = _make_regex_set(@_);
107 43         1997 return qr/^[${set}]*$/u;
108             }
109              
110             sub _make_negative_regex {
111 3     3   8 my $set = _make_regex_set(@_);
112 3         84 return qr/([^${set}])/up;
113             }
114              
115              
116             sub all_perl_files_scripts_ok {
117 1     1 1 187655 my $options = { };
118 1 50       11 $options = shift if ref $_[0] eq 'HASH';
119 1         9 my @files = manifest_files( \&is_perl_file);
120 1         4927 foreach my $file (@files) {
121 23         890 file_scripts_ok( $file, $options );
122             }
123             }
124              
125             sub _is_perl_file {
126 0     0     my ($file) = @_;
127 0   0       return is_perl_file($file) || _is_pod_file($file) || _is_perl_config($file) || _is_xs_file($file) || _is_template($file);
128             }
129              
130             sub _is_pod_file {
131 0     0     $_[0] =~ /\.pod$/i;
132             }
133              
134             sub _is_perl_config {
135 0     0     my ($file) = @_;
136 0           my $name = basename($file);
137 0 0         return 1 if $name =~ /^(?:Rexfile|cpanfile)$/;
138 0           return;
139             }
140              
141             sub _is_xs_file {
142 0     0     $_[0] =~ /\.(c|h|xs)$/i;
143             }
144              
145             sub _is_template {
146 0     0     my ($file) = @_;
147 0           my $name = basename($file);
148 0 0         return 1 if $name =~ /\.(?:epl?|inc|mc|psp|tal|tm?pl|tt)$/;
149             }
150              
151             1;
152              
153             __END__