view release on metacpan or search on metacpan
bin/fuzz-harness-generator view on Meta::CPAN
}
$t .= "\nplan tests => $test_count;\n\n";
for my $i (0 .. $#tests) {
my $test = $tests[$i];
my $n = $i + 1;
my $input = _format_input($test->{'input'});
my $label = "$test->{'method'} does not die on input from $test->{'file'}";
# Flatten and escape the original error for use as a comment
(my $orig_error = $test->{'error'} // '') =~ s/\n/ /g;
$orig_error =~ s/'/\\'/g;
$t .= "# Corpus bug: $orig_error\n";
$t .= "lives_ok { $test->{'module'}\->$test->{'method'}($input) }\n";
$t .= " '$label';\n\n";
}
return $t;
}
bin/fuzz-harness-generator view on Meta::CPAN
# directly in generated test source code.
#
# Entry: $input - the input value to format.
# May be undef, a numeric string,
# or an arbitrary string.
#
# Exit: Returns a Perl literal string:
# 'undef' if $input is undef
# bare number if $input looks numeric
# single-quoted string otherwise, with
# backslashes and single quotes escaped.
#
# Side effects: None.
#
# Notes: Only scalar inputs are handled â corpus
# entries with arrayref or hashref inputs
# are not currently supported and will be
# formatted as a single-quoted string of
# the stringified reference, which will
# not reproduce the original input.
# --------------------------------------------------
sub _format_input {
my ($input) = @_;
return 'undef' unless defined $input;
# Emit bare numeric literals without quoting
return $input if $input =~ /^-?(?:\d+\.?\d*|\.\d+)$/;
# Escape backslashes first, then single quotes, to avoid
# double-escaping when both appear in the same string
(my $escaped = $input) =~ s/\\/\\\\/g;
$escaped =~ s/'/\\'/g;
return "'$escaped'";
}
# --------------------------------------------------
# _infer_module_from_schema
#
# Purpose: Attempt to determine the Perl module
# name for a given corpus method by
# locating and reading the companion YAML
# schema file that sits alongside the
# corpus directory.
bin/test-generator-index view on Meta::CPAN
use Getopt::Long qw(GetOptions);
use IPC::Run3;
use JSON::MaybeXS;
use List::Util qw(max min);
use POSIX qw(strftime);
use HTML::Entities;
use HTTP::Tiny;
use Readonly;
use Storable qw(dclone);
use Time::HiRes qw(sleep);
use URI::Escape qw(uri_escape);
use version;
use WWW::RT::CPAN;
use YAML::XS qw(LoadFile);
=head1 NAME
test-generator-index - Test coverage dashboard generator
=head1 DESCRIPTION
bin/test-generator-index view on Meta::CPAN
# Now calculate deltas and create JavaScript data points
my @data_points;
my $prev_pct;
foreach my $point (@data_points_with_time) {
my $delta = defined $prev_pct ? sprintf('%.1f', $point->{pct} - $prev_pct) : 0;
$prev_pct = $point->{pct};
my $color = $delta > 0 ? 'green' : $delta < 0 ? 'red' : 'gray';
my $comment = js_escape($point->{comment});
push @data_points, qq{{ x: "$point->{timestamp}", y: $point->{pct}, delta: $delta, url: "$point->{url}", label: "$point->{timestamp}", pointBackgroundColor: "$color", comment: "$comment" }};
}
if(scalar(@data_points)) {
push @html, <<'HTML';
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1em;">
<div>
<h2>Coverage Trend</h2>
<label>
<input type="checkbox" id="toggleTrend" checked>
bin/test-generator-index view on Meta::CPAN
'</p>';
} else {
push @html, "<p>No issues active on <a href=\"$rt_url\">RT</a></p>";
}
}
# -------------------------------
# CPAN Testers failing reports table
# -------------------------------
my $dist_name = $config{github_repo};
my $cpan_api = "https://api.cpantesters.org/v3/summary/" . uri_escape($dist_name);
my $http = HTTP::Tiny->new(agent => 'cpan-coverage-html/1.0', timeout => 30);
my $retry = 0;
my $success = 0;
my $res;
# Try a number of times because the cpantesters website can get overloaded
while($retry < $config{max_retry}) {
bin/test-generator-index view on Meta::CPAN
sub run_git {
my @cmd = @_;
my ($out, $err);
run3 ['git', @cmd], \undef, \$out, \$err;
return unless $? == 0;
chomp $out;
return $out;
}
# --------------------------------------------------
# js_escape
#
# Purpose: Escape a string for safe embedding in a
# JavaScript double-quoted string literal
# in generated HTML.
#
# Entry: $str - the string to escape.
#
# Exit: Returns the escaped string. Backslashes
# are doubled, double quotes are escaped,
# and newlines are replaced with \n.
#
# Side effects: None.
#
# Notes: Does not escape single quotes or other
# JS metacharacters â only the minimum
# needed for double-quoted string context.
# --------------------------------------------------
sub js_escape {
my $str = $_[0];
$str =~ s/\\/\\\\/g;
$str =~ s/"/\\"/g;
$str =~ s/\n/\\n/g;
return $str;
}
# --------------------------------------------------
# fetch_reports_by_grades
#
bin/test-generator-index view on Meta::CPAN
# in a single request.
# --------------------------------------------------
sub fetch_reports_by_grades {
my ($dist, $version, @grades) = @_;
my %seen;
my @reports;
for my $grade (@grades) {
my $url = 'https://api.cpantesters.org/v3/summary/'
. uri_escape($dist)
. '/' . uri_escape($version)
. "?grade=$grade";
my $res = $http->get($url);
next unless $res->{success};
my $arr = eval { decode_json($res->{content}) };
next unless ref $arr eq 'ARRAY';
for my $r (@$arr) {
my $key = make_key($r);
lib/App/Test/Generator.pm view on Meta::CPAN
# No positional arguments found in any field
return 0;
}
# --------------------------------------------------
# q_wrap
#
# Purpose: Wrap a string in the most readable
# q{} form that does not require escaping,
# falling back to single-quoted form with
# escaped apostrophes if no delimiter is
# available.
#
# Entry: $s - the string to wrap. May be undef.
# Exit: Returns a Perl source-code fragment that
# evaluates to the original string value,
# or the string 'undef' if $s is undef.
#
# Side effects: None.
#
# Notes: index() returns -1 when not found and
lib/App/Test/Generator.pm view on Meta::CPAN
}
# Try single-character delimiters in preference order
for my $d (@Q_SINGLE_DELIMITERS) {
# index() returns $INDEX_NOT_FOUND (-1) when not found.
# Must use != $INDEX_NOT_FOUND rather than > 0 since
# the delimiter may legitimately appear at position 0
return "q$d$s$d" if index($s, $d) == $INDEX_NOT_FOUND;
}
# Last resort â single-quoted string with escaped apostrophes
(my $esc = $s) =~ s/'/\\'/g;
return "'$esc'";
}
# --------------------------------------------------
# perl_sq
#
# Purpose: Escape a string for safe inclusion
# inside a single-quoted Perl string
# literal in generated test code.
#
# Entry: $s - the string to escape.
# Exit: Returns the escaped string, or an
# empty string if $s is undef.
#
# Side effects: None.
#
# Notes: NUL byte replacement produces the
# two-character sequence \0 which is
# only correct when the result is used
# inside a double-quoted string context
# in the generated test.
#
lib/App/Test/Generator.pm view on Meta::CPAN
sub perl_sq {
my $s = $_[0];
croak('perl_sq: argument must be a plain string, not a reference') if ref($s);
# Return empty string for undef â callers that need
# 'undef' literal should use perl_quote instead
return '' unless defined $s;
# Escape backslashes first so later substitutions
# don't double-escape already-escaped sequences
$s =~ s/\\/\\\\/g;
# Escape apostrophes so they don't terminate the
# surrounding single-quoted string literal
$s =~ s/'/\\'/g;
# Escape common control characters to their
# printable two-character escape sequences
$s =~ s/\n/\\n/g;
$s =~ s/\r/\\r/g;
$s =~ s/\t/\\t/g;
$s =~ s/\f/\\f/g;
# Replace NUL bytes with \0 â valid only in
# double-quoted string context in generated code
$s =~ s/\0/\\0/g;
return $s;
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
=item * Parameter lists: C<$param - type, default 'value'>
=back
=head3 Value Processing
Properly handles:
=over 4
=item * String literals with quotes and escape sequences
=item * Numeric values (integers and floats)
=item * Boolean values (true/false converted to 1/0)
=item * Empty data structures ([] and {})
=item * Special values (undef, __PACKAGE__)
=item * Complex expressions (preserved as-is when unevaluatable)
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
return unless defined $param && $param =~ /^\w+$/;
# ref() check for CODE
if ($code =~ /ref\s*\(\s*\$$param\s*\)\s*eq\s*['"]CODE['"]/i) {
$p->{type} = 'coderef';
$p->{semantic} = 'callback';
$self->_log(" ADVANCED: $param is coderef (ref check)");
return;
}
# Invocation as coderef - note the escaped @ in \@_
if ($code =~ /\$$param\s*->\s*\(/ ||
$code =~ /\$$param\s*->\s*\(\s*\@_\s*\)/ ||
$code =~ /&\s*\{\s*\$$param\s*\}/) {
$p->{type} = 'coderef';
$p->{semantic} = 'callback';
$self->_log(" ADVANCED: $param invoked as coderef");
return;
}
# Parameter name suggests callback
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
# extracted from code or POD into a
# clean Perl scalar, handling quoted
# strings, numeric literals, boolean
# keywords, empty containers, and
# undef.
#
# Entry: $value - raw value string.
# May be undef.
# $from_code - true if the value was
# extracted from source
# code (affects escape
# sequence handling).
#
# Exit: Returns the cleaned value:
# undef for undef or unparseable
# {} for empty hashrefs
# [] for empty arrayrefs
# integer for whole numbers
# float for decimal numbers
# 1 or 0 for boolean keywords
# string for everything else
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
$value = $1;
} elsif ($value =~ /^q[qwx]?\s*([^a-zA-Z0-9\{\[])(.*?)\1$/s) {
$value = $2;
}
# Handle quoted strings
if ($value =~ /^(['"])(.*)\1$/s) {
$value = $2;
if ($from_code) {
# In regex captures from source code, escape sequences are doubled
# \\n in capture needs to become \n for the test
$value =~ s/\\\\/\\/g;
}
# Only unescape the quote characters themselves
$value =~ s/\\"/"/g;
$value =~ s/\\'/'/g;
# If NOT from code (i.e., from POD), interpret escape sequences
unless ($from_code) {
$value =~ s/\\n/\n/g;
$value =~ s/\\r/\r/g;
$value =~ s/\\t/\t/g;
$value =~ s/\\\\/\\/g;
}
}
# Sometimes trailing ) is left on
if($value !~ /^\(/) {
t/Generator.t view on Meta::CPAN
*_get_dominant_type = \&App::Test::Generator::_get_dominant_type;
*_detect_transform_properties = \&App::Test::Generator::_detect_transform_properties;
*_render_properties = \&App::Test::Generator::_render_properties;
*_schema_to_lectrotest_generator = \&App::Test::Generator::_schema_to_lectrotest_generator;
*_get_semantic_generators = \&App::Test::Generator::_get_semantic_generators;
*_get_builtin_properties = \&App::Test::Generator::_get_builtin_properties;
*_is_perl_builtin = \&App::Test::Generator::_is_perl_builtin;
}
# ------------------------------------------------------------------
# perl_sq â escape a string for single-quoted Perl string context
# ------------------------------------------------------------------
subtest 'perl_sq() returns empty string for undef' => sub {
is(App::Test::Generator::perl_sq(undef), '', 'undef produces empty string');
};
subtest 'perl_sq() escapes backslashes first' => sub {
is(App::Test::Generator::perl_sq('a\\b'), 'a\\\\b', 'backslash doubled');
};
subtest 'perl_sq() escapes apostrophes' => sub {
is(App::Test::Generator::perl_sq("it's"), "it\\'s", 'apostrophe escaped');
};
subtest 'perl_sq() escapes common control characters' => sub {
is(App::Test::Generator::perl_sq("a\nb"), 'a\\nb', 'newline escaped');
is(App::Test::Generator::perl_sq("a\rb"), 'a\\rb', 'CR escaped');
is(App::Test::Generator::perl_sq("a\tb"), 'a\\tb', 'tab escaped');
is(App::Test::Generator::perl_sq("a\fb"), 'a\\fb', 'formfeed escaped');
};
subtest 'perl_sq() replaces NUL bytes' => sub {
is(App::Test::Generator::perl_sq("a\0b"), 'a\\0b', 'NUL replaced with \\0');
};
subtest 'perl_sq() leaves plain string unchanged' => sub {
is(App::Test::Generator::perl_sq('hello'), 'hello', 'plain string unchanged');
};
t/Generator.t view on Meta::CPAN
subtest 'q_wrap() falls back when curly braces in string' => sub {
my $result = App::Test::Generator::q_wrap('a{b}c');
# Should use a different delimiter
unlike($result, qr/^q\{a\{b\}c\}$/, 'does not use q{} when string contains {}');
ok(length($result) > 0, 'returns non-empty result');
};
subtest 'q_wrap() uses single-quote fallback when all delimiters used' => sub {
# A string containing all bracket pairs AND all single-char delimiters
# forces the escaped single-quote fallback
my $str = '{}()[]<>~!%^=+:,;|/#';
my $result = App::Test::Generator::q_wrap($str);
ok(defined $result, 'returns defined value for pathological string');
};
subtest 'q_wrap() correctly uses != INDEX_NOT_FOUND boundary' => sub {
# A string starting with ~ means index returns 0 (not -1)
# If the guard were "> 0" instead of "!= -1" it would wrongly
# choose ~ as the delimiter when ~ is at position 0
my $result = App::Test::Generator::q_wrap('~starts with tilde');
t/default_value_extraction.t view on Meta::CPAN
'test',
'Cleans string with whitespace'
);
is(
$extractor->_clean_default_value(' 42 '),
42,
'Cleans integer with whitespace'
);
# Test escaped strings
is(
$extractor->_clean_default_value('"line1\\nline2"'),
"line1\nline2",
'Handles escaped newlines'
);
is(
$extractor->_clean_default_value("'it\\'s working'"),
"it's working",
'Handles escaped quotes'
);
done_testing();
};
# POD default value extraction
subtest 'POD Default Value Extraction' => sub {
my $module = <<'END_MODULE';
package Test::PODDefaults;
use strict;
t/default_value_extraction.t view on Meta::CPAN
package Test::EdgeCases;
use strict;
use warnings;
sub edge_cases {
my ($self, $param1, $param2, $param3, $param4, $param5) = @_;
# Edge case 1: Default with quotes inside quotes
$param1 = $param1 || "it's complicated";
# Edge case 2: Default with escaped characters
$param2 //= "line1\\nline2\\ttab";
# Edge case 3: Default as expression in parentheses
$param3 = defined $param3 ? $param3 : (10 + 20);
# Edge case 4: Default with trailing comment
$param4 = $param4 || 'default'; # this is a comment
# Edge case 5: Default with q// operator
$param5 = $param5 || q{default value};
t/default_value_extraction.t view on Meta::CPAN
# Check specific edge cases
is(
$code_params->{param1}{_default},
"it's complicated",
'Handles quotes inside string default'
);
is(
$code_params->{param2}{_default},
"line1\\nline2\\ttab",
'Preserves escaped characters in default'
);
is(
$code_params->{param2}{_default},
"line1\\nline2\\ttab",
'Preserves escaped characters in default'
);
# Note: param3 returns expression "(10 + 20)" which we can't evaluate
ok(
$code_params->{param3}{_default},
'Extracts expression default (even if unevaluatable)'
);
is(
$code_params->{param4}{_default},
t/extended_tests.t view on Meta::CPAN
like($result, qr/i/, 'case-insensitive modifier included');
};
subtest 'perl_quote: hashref falls through to render_fallback' => sub {
my $result = App::Test::Generator::perl_quote({ key => 'val' });
ok(defined $result, 'hashref handled');
like($result, qr/key/, 'key present in output');
like($result, qr/val/, 'value present in output');
};
subtest 'perl_sq: backslash escaped correctly' => sub {
my $result = App::Test::Generator::perl_sq('a\\b');
like($result, qr/\\\\/, 'backslash doubled');
};
subtest 'perl_sq: single quote escaped correctly' => sub {
my $result = App::Test::Generator::perl_sq("it's");
like($result, qr/\\'/, 'apostrophe escaped');
};
subtest 'perl_sq: control characters escaped' => sub {
is(App::Test::Generator::perl_sq("\n"), '\\n', 'newline escaped');
is(App::Test::Generator::perl_sq("\t"), '\\t', 'tab escaped');
is(App::Test::Generator::perl_sq("\r"), '\\r', 'CR escaped');
};
subtest 'perl_sq: NUL byte escaped as \\0' => sub {
is(App::Test::Generator::perl_sq("\0"), '\\0', 'NUL escaped');
};
subtest 'q_wrap: prefers bracket form when available' => sub {
my $result = App::Test::Generator::q_wrap('hello');
like($result, qr/^q\{hello\}$/, 'bracket form preferred');
};
subtest 'q_wrap: falls back to () when {} used in string' => sub {
my $result = App::Test::Generator::q_wrap('a{b}c');
ok(defined $result, 'string with braces handled');
t/function.t view on Meta::CPAN
# Constants used across multiple subtests to avoid
# magic literals and make intent clear
# --------------------------------------------------
Readonly my $EMPTY_STRING => '';
Readonly my $UNDEF_LITERAL => 'undef';
# ==================================================================
# perl_sq
# --------------------------------------------------
# White-box tests for the low-level single-quote
# string escaper used by perl_quote and q_wrap
# ==================================================================
subtest 'perl_sq' => sub {
# Access the private function directly via the package namespace
my $fn = \&App::Test::Generator::perl_sq;
# Undef input returns empty string, not 'undef'
is($fn->(undef), $EMPTY_STRING, 'undef returns empty string');
# Plain ASCII string passes through unchanged
is($fn->('hello'), 'hello', 'plain ASCII unchanged');
# Apostrophe must be escaped so it does not break the surrounding
# single-quoted string literal in the generated test
is($fn->("it's"), "it\\'s", 'apostrophe escaped');
# Backslash must be escaped first so later substitutions
# do not double-escape already-escaped sequences
is($fn->('a\\b'), 'a\\\\b', 'backslash escaped');
# Control characters are converted to their two-char sequences
is($fn->("a\nb"), 'a\\nb', 'newline escaped');
is($fn->("a\rb"), 'a\\rb', 'carriage return escaped');
is($fn->("a\tb"), 'a\\tb', 'tab escaped');
is($fn->("a\fb"), 'a\\fb', 'form feed escaped');
# NUL byte is converted to \0 for double-quoted context
is($fn->("a\0b"), 'a\\0b', 'NUL byte escaped');
# Both apostrophe and backslash in the same string
is($fn->("a\\'b"), "a\\\\\\'b", 'backslash and apostrophe together');
done_testing();
};
# ==================================================================
# perl_quote
# --------------------------------------------------
t/function.t view on Meta::CPAN
is($fn->(0), '0', 'zero unquoted');
is($fn->(42), '42', 'positive integer unquoted');
is($fn->(-1), '-1', 'negative integer unquoted');
# Floats are emitted unquoted
is($fn->(3.14), '3.14', 'float unquoted');
# Plain strings are single-quoted
is($fn->('hello'), "'hello'", 'string single-quoted');
# Strings containing apostrophes have them escaped
is($fn->("it's"), "'it\\'s'", 'apostrophe in string escaped');
# Arrayrefs are recursively quoted with brackets
is($fn->([1, 2, 3]), '[ 1, 2, 3 ]', 'arrayref recursively quoted');
# Nested arrayrefs recurse correctly
is($fn->([1, [2, 3]]), '[ 1, [ 2, 3 ] ]', 'nested arrayref quoted');
# Arrayref containing undef produces undef literal in the output
is($fn->([undef, 1]), "[ $UNDEF_LITERAL, 1 ]", 'arrayref with undef element');
t/function.t view on Meta::CPAN
unlike($fn->($with_brace), qr/^q\{/, 'string with { avoids q{}');
# String containing all bracket pairs falls back to single chars
my $all_brackets = '{([<>])}';
my $result = $fn->($all_brackets);
like($result, qr/^q./, 'all-bracket string still uses q form');
# Empty string produces empty q form
is($fn->($EMPTY_STRING), 'q{}', 'empty string produces q{}');
# String with apostrophe â q_wrap avoids needing to escape it
# by choosing a delimiter that is not an apostrophe
my $apos = "it's";
my $wrapped = $fn->($apos);
unlike($wrapped, qr/\\'/, 'apostrophe not escaped in q_wrap output');
done_testing();
};
# ==================================================================
# render_fallback
# --------------------------------------------------
# Tests for the Data::Dumper-based catch-all renderer
# ==================================================================
subtest 'render_fallback' => sub {