summaryrefslogtreecommitdiff
path: root/ext/XS-APItest/t/handy.t
blob: f21a39d3bdc40b5c8169128c5ee214ecffb22f3d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
#!perl -w

BEGIN {
    require 'loc_tools.pl';   # Contains locales_enabled() and
                              # find_utf8_ctype_locale()
}

use strict;
use Test::More;
use Config;

use XS::APItest;

my $tab = " " x 4;  # Indent subsidiary tests this much

use Unicode::UCD qw(search_invlist prop_invmap prop_invlist);
my ($charname_list, $charname_map, $format, $default) = prop_invmap("Name Alias");

sub get_charname($) {
    my $cp = shift;

    # If there is a an abbreviation for the code point name, use it
    my $name_index = search_invlist(\@{$charname_list}, $cp);
    if (defined $name_index) {
        my $synonyms = $charname_map->[$name_index];
        if (ref $synonyms) {
            my $pat = qr/: abbreviation/;
            my @abbreviations = grep { $_ =~ $pat } @$synonyms;
            if (@abbreviations) {
                return $abbreviations[0] =~ s/$pat//r;
            }
        }
    }

    # Otherwise, use the full name
    use charnames ();
    return charnames::viacode($cp) // "No name";
}

sub truth($) {  # Converts values so is() works
    return (shift) ? 1 : 0;
}

my $base_locale;
my $utf8_locale;
if(locales_enabled('LC_ALL')) {
    require POSIX;
    $base_locale = POSIX::setlocale( &POSIX::LC_ALL, "C");
    if (defined $base_locale && $base_locale eq 'C') {
        use locale; # make \w work right in non-ASCII lands

        # Some locale implementations don't have the 128-255 characters all
        # mean nothing.  Skip the locale tests in that situation
        for my $i (128 .. 255) {
            if (chr(utf8::unicode_to_native($i)) =~ /[[:print:]]/) {
                undef $base_locale;
                last;
            }
        }

        $utf8_locale = find_utf8_ctype_locale() if $base_locale;
    }
}

sub get_display_locale_or_skip($$) {

    # Helper function intimately tied to its callers.  It knows the loop
    # iterates with a locale of "", meaning don't use locale; $base_locale
    # meaning to use a non-UTF-8 locale; and $utf8_locale.
    #
    # It checks to see if the current test should be skipped or executed,
    # returning an empty list for the former, and for the latter:
    #   ( 'locale display name',
    #     bool of is this a UTF-8 locale )
    #
    # The display name is the empty string if not using locale.  Functions
    # with _LC in their name are skipped unless in locale, and functions
    # without _LC are executed only outside locale.  However, if no locales at
    # all are on the system, the _LC functions are executed outside locale.

    my ($locale, $suffix) = @_;

    # The test should be skipped if the input is for a non-existent locale
    return unless defined $locale;

    # Here the input is defined, either a locale name or "".  If the test is
    # for not using locales, we want to do the test for non-LC functions,
    # and skip it for LC ones (except if there are no locales on the system,
    # we do it for LC ones as if they weren't LC).
    if ($locale eq "") {
        return ("", 0) if $suffix !~ /LC/ || ! defined $base_locale;
        return;
    }

    # Here the input is for a real locale.  We don't test the non-LC functions
    # for locales.
    return if $suffix !~ /LC/;

    # Here is for a LC function and a real locale.  The base locale is not
    # UTF-8.
    return (" ($locale locale)", 0) if $locale eq $base_locale;

    # The only other possibility is that we have a UTF-8 locale
    return (" ($locale)", 1);
}

sub try_malforming($$$)
{
    # Determines if the tests for malformed UTF-8 should be done.  When done,
    # the .xs code creates malformations by pretending the length is shorter
    # than it actually is.  Some things can't be malformed, and sometimes this
    # test knows that the current code doesn't look for a malformation under
    # various circumstances.

    my ($i, $function, $using_locale) = @_;

    # Single bytes can't be malformed
    return 0 if $i < ((ord "A" == 65) ? 128 : 160);

    # ASCII doesn't need to ever look beyond the first byte.
    return 0 if $function eq "ASCII";

    # No controls above 255, so the code doesn't look at those
    return 0 if $i > 255 && $function eq "CNTRL";

    # No non-ASCII digits below 256, except if using locales.
    return 0 if $i < 256 && ! $using_locale && $function =~ /X?DIGIT/;

    return 1;
}

my %properties = (
                   # name => Lookup-property name
                   alnum => 'Word',
                   wordchar => 'Word',
                   alphanumeric => 'Alnum',
                   alpha => 'XPosixAlpha',
                   ascii => 'ASCII',
                   blank => 'Blank',
                   cntrl => 'Control',
                   digit => 'Digit',
                   graph => 'Graph',
                   idfirst => '_Perl_IDStart',
                   idcont => '_Perl_IDCont',
                   lower => 'XPosixLower',
                   print => 'Print',
                   psxspc => 'XPosixSpace',
                   punct => 'XPosixPunct',
                   quotemeta => '_Perl_Quotemeta',
                   space => 'XPerlSpace',
                   vertws => 'VertSpace',
                   upper => 'XPosixUpper',
                   xdigit => 'XDigit',
                );

my %seen;
my @warnings;
local $SIG{__WARN__} = sub { push @warnings, @_ };

my %utf8_param_code = (
                        "_safe"                 =>  0,
                        "_safe, malformed"      =>  1,
                        "deprecated unsafe"     => -1,
                      );

foreach my $name (sort keys %properties, 'octal') {
    my @invlist;
    if ($name eq 'octal') {
        # Hand-roll an inversion list with 0-7 in it and nothing else.
        push @invlist, ord "0", ord "8";
    }
    else {
        my $property = $properties{$name};
        @invlist = prop_invlist($property, '_perl_core_internal_ok');
        if (! @invlist) {

            # An empty return could mean an unknown property, or merely that
            # it is empty.  Call in scalar context to differentiate
            if (! prop_invlist($property, '_perl_core_internal_ok')) {
                fail("No inversion list found for $property");
                next;
            }
        }
    }

    # Include all the Latin1 code points, plus 0x100.
    my @code_points = (0 .. 256);

    # Then include the next few boundaries above those from this property
    my $above_latins = 0;
    foreach my $range_start (@invlist) {
        next if $range_start < 257;
        push @code_points, $range_start - 1, $range_start;
        $above_latins++;
        last if $above_latins > 5;
    }

    # This makes sure we are using the Perl definition of idfirst and idcont,
    # and not the Unicode.  There are a few differences.
    push @code_points, ord "\N{ESTIMATED SYMBOL}" if $name =~ /^id(first|cont)/;
    if ($name eq "idcont") {    # And some that are continuation but not start
        push @code_points, ord("\N{GREEK ANO TELEIA}"),
                           ord("\N{COMBINING GRAVE ACCENT}");
    }

    # And finally one non-Unicode code point.
    push @code_points, 0x110000;    # Above Unicode, no prop should match
    no warnings 'non_unicode';

    for my $j (@code_points) {
        my $i = utf8::native_to_unicode($j);
        my $function = uc($name);

        is (@warnings, 0, "Got no unexpected warnings in previous iteration")
           or diag("@warnings");
        undef @warnings;

        my $matches = search_invlist(\@invlist, $i);
        if (! defined $matches) {
            $matches = 0;
        }
        else {
            $matches = truth(! ($matches % 2));
        }

        my $ret;
        my $char_name = get_charname($j);
        my $display_name = sprintf "\\x{%02X, %s}", $i, $char_name;
        my $display_call = "is${function}( $display_name )";

        foreach my $suffix ("", "_A", "_L1", "_LC", "_uni", "_uvchr",
                            "_LC_uvchr", "_utf8", "_LC_utf8")
        {

            # Not all possible macros have been defined
            if ($name eq 'vertws') {

                # vertws is always all of Unicode
                next if $suffix !~ / ^ _ ( uni | uvchr | utf8 ) $ /x;
            }
            elsif ($name eq 'alnum') {

                # ALNUM_A, ALNUM_L1, and ALNUM_uvchr are not defined as these
                # suffixes were added later, after WORDCHAR was created to be
                # a clearer synonym for ALNUM
                next if    $suffix eq '_A'
                        || $suffix eq '_L1'
                        || $suffix eq '_uvchr';
            }
            elsif ($name eq 'octal') {
                next if $suffix ne ""  && $suffix ne '_A' && $suffix ne '_L1';
            }
            elsif ($name eq 'quotemeta') {
                # There is only one macro for this, and is defined only for
                # Latin1 range
                next if $suffix ne ""
            }

            foreach my $locale ("", $base_locale, $utf8_locale) {

                my ($display_locale, $locale_is_utf8)
                                = get_display_locale_or_skip($locale, $suffix);
                next unless defined $display_locale;

                use if $locale, "locale";
                POSIX::setlocale( &POSIX::LC_ALL, $locale) if $locale;

                if ($suffix !~ /utf8/) {    # _utf8 has to handled specially
                    my $display_call
                       = "is${function}$suffix( $display_name )$display_locale";
                    $ret = truth eval "test_is${function}$suffix($i)";
                    if (is ($@, "", "$display_call didn't give error")) {
                        my $truth = $matches;
                        if ($truth) {

                            # The single byte functions are false for
                            # above-Latin1
                            if ($i >= 256) {
                                $truth = 0
                                        if $suffix=~ / ^ ( _A | _L [1C] )? $ /x;
                            }
                            elsif (   utf8::native_to_unicode($i) >= 128
                                   && $name ne 'quotemeta')
                            {

                                # The no-suffix and _A functions are false
                                # for non-ASCII.  So are  _LC  functions on a
                                # non-UTF-8 locale
                                $truth = 0 if    $suffix eq "_A"
                                              || $suffix eq ""
                                              || (     $suffix =~ /LC/
                                                  && ! $locale_is_utf8);
                            }
                        }

                        is ($ret, $truth, "${tab}And correctly returns $truth");
                    }
                }
                else {  # _utf8 suffix
                    my $char = chr($i);
                    utf8::upgrade($char);
                    $char = quotemeta $char if $char eq '\\' || $char eq "'";
                    my $truth;
                    if (   $suffix =~ /LC/
                        && ! $locale_is_utf8
                        && $i < 256
                        && utf8::native_to_unicode($i) >= 128)
                    {   # The C-locale _LC function returns FALSE for Latin1
                        # above ASCII
                        $truth = 0;
                    }
                    else {
                        $truth = $matches;
                    }

                    foreach my $utf8_param("_safe",
                                           "_safe, malformed",
                                           "deprecated unsafe"
                                          )
                    {
                        my $utf8_param_code = $utf8_param_code{$utf8_param};
                        my $expect_error = $utf8_param_code > 0;
                        next if      $expect_error
                                && ! try_malforming($i, $function,
                                                    $suffix =~ /LC/);

                        my $display_call = "is${function}$suffix( $display_name"
                                         . ", $utf8_param )$display_locale";
                        $ret = truth eval "test_is${function}$suffix('$char',"
                                        . " $utf8_param_code)";
                        if ($expect_error) {
                            isnt ($@, "",
                                    "expected and got error in $display_call");
                            like($@, qr/Malformed UTF-8 character/,
                                "${tab}And got expected message");
                            if (is (@warnings, 1,
                                           "${tab}Got a single warning besides"))
                            {
                                like($warnings[0],
                                     qr/Malformed UTF-8 character.*short/,
                                     "${tab}Got expected warning");
                            }
                            else {
                                diag("@warnings");
                            }
                            undef @warnings;
                        }
                        elsif (is ($@, "", "$display_call didn't give error")) {
                            is ($ret, $truth,
                                "${tab}And correctly returned $truth");
                            if ($utf8_param_code < 0) {
                                my $warnings_ok;
                                my $unique_function = "is" . $function . $suffix;
                                if (! $seen{$unique_function}++) {
                                    $warnings_ok = is(@warnings, 1,
                                        "${tab}This is first call to"
                                      . " $unique_function; Got a single"
                                      . " warning");
                                    if ($warnings_ok) {
                                        $warnings_ok = like($warnings[0],
                qr/starting in Perl .* will require an additional parameter/,
                                            "${tab}The warning was the expected"
                                          . " deprecation one");
                                    }
                                }
                                else {
                                    $warnings_ok = is(@warnings, 0,
                                        "${tab}This subsequent call to"
                                      . " $unique_function did not warn");
                                }
                                $warnings_ok or diag("@warnings");
                                undef @warnings;
                            }
                        }
                    }
                }
            }
        }
    }
}

my %to_properties = (
                FOLD  => 'Case_Folding',
                LOWER => 'Lowercase_Mapping',
                TITLE => 'Titlecase_Mapping',
                UPPER => 'Uppercase_Mapping',
            );


foreach my $name (sort keys %to_properties) {
    my $property = $to_properties{$name};
    my ($list_ref, $map_ref, $format, $missing)
                                      = prop_invmap($property, );
    if (! $list_ref || ! $map_ref) {
        fail("No inversion map found for $property");
        next;
    }
    if ($format !~ / ^ a l? $ /x) {
        fail("Unexpected inversion map format ('$format') found for $property");
        next;
    }

    # Include all the Latin1 code points, plus 0x100.
    my @code_points = (0 .. 256);

    # Then include the next few multi-char folds above those from this
    # property, and include the next few single folds as well
    my $above_latins = 0;
    my $multi_char = 0;
    for my $i (0 .. @{$list_ref} - 1) {
        my $range_start = $list_ref->[$i];
        next if $range_start < 257;
        if (ref $map_ref->[$i] && $multi_char < 5)  {
            push @code_points, $range_start - 1
                                        if $code_points[-1] != $range_start - 1;
            push @code_points, $range_start;
            $multi_char++;
        }
        elsif ($above_latins < 5) {
            push @code_points, $range_start - 1
                                        if $code_points[-1] != $range_start - 1;
            push @code_points, $range_start;
            $above_latins++;
        }
        last if $above_latins >= 5 && $multi_char >= 5;
    }

    # And finally one non-Unicode code point.
    push @code_points, 0x110000;    # Above Unicode, no prop should match
    no warnings 'non_unicode';

    # $j is native; $i unicode.
    for my $j (@code_points) {
        my $i = utf8::native_to_unicode($j);
        my $function = $name;

        my $index = search_invlist(\@{$list_ref}, $j);

        my $ret;
        my $char_name = get_charname($j);
        my $display_name = sprintf "\\N{U+%02X, %s}", $i, $char_name;

        foreach my $suffix ("", "_L1", "_LC") {

            # This is the only macro defined for L1
            next if $suffix eq "_L1" && $function ne "LOWER";

          SKIP:
            foreach my $locale ("", $base_locale, $utf8_locale) {

                # titlecase is not defined in locales.
                next if $name eq 'TITLE' && $suffix eq "_LC";

                my ($display_locale, $locale_is_utf8)
                                = get_display_locale_or_skip($locale, $suffix);
                next unless defined $display_locale;

                skip("to${name}_LC does not work for LATIN SMALL LETTER SHARP S"
                  . "$display_locale", 1)
                            if  $i == 0xDF && $name =~ / FOLD | UPPER /x
                             && $suffix eq "_LC" && $locale_is_utf8;

                use if $locale, "locale";
                POSIX::setlocale( &POSIX::LC_ALL, $locale) if $locale;

                my $display_call = "to${function}$suffix("
                                 . " $display_name )$display_locale";
                $ret = eval "test_to${function}$suffix($j)";
                if (is ($@, "", "$display_call didn't give error")) {
                    my $should_be;
                    if ($i > 255) {
                        $should_be = $j;
                    }
                    elsif (    $i > 127
                            && (   $suffix eq ""
                                || ($suffix eq "_LC" && ! $locale_is_utf8)))
                    {
                        $should_be = $j;
                    }
                    elsif ($map_ref->[$index] != $missing) {
                        $should_be = $map_ref->[$index] + $j - $list_ref->[$index]
                    }
                    else {
                        $should_be = $j;
                    }

                    is ($ret, $should_be,
                        sprintf("${tab}And correctly returned 0x%02X",
                                                              $should_be));
                }
            }
        }

        # The _uni, uvchr, and _utf8 functions return both the ordinal of the
        # first code point of the result, and the result in utf8.  The .xs
        # tests return these in an array, in [0] and [1] respectively, with
        # [2] the length of the utf8 in bytes.
        my $utf8_should_be = "";
        my $first_ord_should_be;
        if (ref $map_ref->[$index]) {   # A multi-char result
            for my $j (0 .. @{$map_ref->[$index]} - 1) {
                $utf8_should_be .= chr $map_ref->[$index][$j];
            }

            $first_ord_should_be = $map_ref->[$index][0];
        }
        else {  # A single-char result
            $first_ord_should_be = ($map_ref->[$index] != $missing)
                                    ? $map_ref->[$index] + $j
                                                         - $list_ref->[$index]
                                    : $j;
            $utf8_should_be = chr $first_ord_should_be;
        }
        utf8::upgrade($utf8_should_be);

        # Test _uni, uvchr
        foreach my $suffix ('_uni', '_uvchr') {
            my $s;
            my $len;
            my $display_call = "to${function}$suffix( $display_name )";
            $ret = eval "test_to${function}$suffix($j)";
            if (is ($@, "", "$display_call didn't give error")) {
                is ($ret->[0], $first_ord_should_be,
                    sprintf("${tab}And correctly returned 0x%02X",
                                                    $first_ord_should_be));
                is ($ret->[1], $utf8_should_be, "${tab}Got correct utf8");
                use bytes;
                is ($ret->[2], length $utf8_should_be,
                    "${tab}Got correct number of bytes for utf8 length");
            }
        }

        # Test _utf8
        my $char = chr($j);
        utf8::upgrade($char);
        $char = quotemeta $char if $char eq '\\' || $char eq "'";
        foreach my $utf8_param("_safe",
                                "_safe, malformed",
                                )
        {
            my $utf8_param_code = $utf8_param_code{$utf8_param};
            my $expect_error = $utf8_param_code > 0;

            # Skip if can't malform (because is a UTF-8 invariant)
            next if $expect_error && $i < ((ord "A" == 65) ? 128 : 160);

            my $display_call = "to${function}_utf8($display_name, $utf8_param )";
            $ret = eval   "test_to${function}_utf8('$char', $utf8_param_code)";
            if ($expect_error) {
                isnt ($@, "", "expected and got error in $display_call");
                like($@, qr/Malformed UTF-8 character/,
                     "${tab}And got expected message");
                undef @warnings;
            }
            elsif (is ($@, "", "$display_call didn't give error")) {
                is ($ret->[0], $first_ord_should_be,
                    sprintf("${tab}And correctly returned 0x%02X",
                                                    $first_ord_should_be));
                is ($ret->[1], $utf8_should_be, "${tab}Got correct utf8");
                use bytes;
                is ($ret->[2], length $utf8_should_be,
                    "${tab}Got correct number of bytes for utf8 length");
            }
        }
    }
}

# This is primarily to make sure that no non-Unicode warnings get generated
is(scalar @warnings, 0, "No unexpected warnings were generated in the tests")
  or diag @warnings;

done_testing;