App-Test-Generator
view release on metacpan or search on metacpan
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
my $returns_desc = $1;
$returns_desc =~ s/^\s+|\s+$//g;
$self->_log(" OUTPUT: Found Returns section: $returns_desc");
# Try to infer type from description
if ($returns_desc =~ /\b(string|text)\b/i) {
$output->{type} = 'string';
} elsif ($returns_desc =~ /\b(integer|int|count)\b/i) {
$output->{type} = 'integer';
} elsif ($returns_desc =~ /\b(float|decimal|number)\b/i) {
$output->{type} = 'number';
} elsif ($returns_desc =~ /\b(boolean|true|false)\b/i) {
$output->{type} = 'boolean';
} elsif ($returns_desc =~ /\b(array|list)\b/i) {
$output->{type} = 'arrayref';
} elsif ($returns_desc =~ /\b(hash|hashref|dictionary)\b/i) {
$output->{type} = 'hashref';
} elsif ($returns_desc =~ /\b(object|instance)\b/i) {
$output->{type} = 'object';
} elsif ($returns_desc =~ /\bundef\b/i) {
$output->{type} = 'undef';
}
# Look for specific values
if ($returns_desc =~ /\b1\s+(?:on\s+success|if\s+successful)\b/i) {
$output->{value} = 1;
if(defined($output->{'type'}) && ($output->{type} eq 'scalar')) {
$output->{type} = 'boolean';
} else {
$output->{type} ||= 'boolean';
}
$self->_log(" OUTPUT: Returns 1 on success");
} elsif ($returns_desc =~ /\b0\s+(?:on\s+failure|if\s+fail)\b/i) {
$output->{alt_value} = 0;
} elsif ($returns_desc =~ /dies\s+on\s+(?:error|failure)/i) {
$output->{_STATUS} = 'LIVES';
$self->_log(' OUTPUT: Should not die on success');
}
if ($returns_desc =~ /\b(true|false)\b/i) {
$output->{type} ||= 'boolean';
}
if ($returns_desc =~ /\bundef\b/i) {
$output->{optional} = 1;
}
}
# Pattern 2: Inline "returns X"
if((!$output->{type}) && ($pod =~ /returns?\s+(?:an?\s+)?(\w+)/i)) {
my $type = lc($1);
$type = 'boolean' if $type =~ /^(true|false|bool)$/;
# Skip if it's just a number (like "returns 1")
$type = 'integer' if $type eq 'int';
$type = 'number' if $type =~ /^(num|float)$/;
$type = 'arrayref' if $type eq 'array';
$type = 'hashref' if $type eq 'hash';
if($type =~ /^\d+$/) {
if($type eq '1' || $type eq '0') {
# Try hard to guess if the result is a boolean
if($pod =~ /1 on success.+0 (on|if) /i) {
$type = 'boolean';
} elsif($pod =~ /return 0 .+ 1 on success/) {
$type = 'boolean';
} else {
$type = 'integer';
}
} else {
$type = 'integer';
}
}
$type = 'arrayref' if !$type && $pod =~ /returns?\s+.+\slist\b/i;
# $output->{type} = $type if $type && $type !~ /^\d+$/;
if ($VALID_OUTPUT_TYPES{$type}) {
$output->{type} = $type;
$self->_log(" OUTPUT: Inferred type from POD: $type");
} else {
$self->_log(" OUTPUT: POD return type '$type' is not a valid type, ignoring");
}
}
}
}
# --------------------------------------------------
# _extract_defaults_from_pod
#
# Purpose: Extract default values for parameters
# from POD documentation using multiple
# pattern strategies.
#
# Entry: $pod - POD string for the method.
# May be undef or empty.
#
# Exit: Returns a hashref of parameter name
# to cleaned default value. Returns an
# empty hashref if no POD is provided
# or no defaults are found.
#
# Side effects: None.
#
# Notes: Three strategies are tried: (1) lines
# containing 'Default:' or 'Defaults to:',
# (2) lines containing 'Optional, default',
# (3) inline $name - type, default value
# format. Parameter names are inferred
# by scanning backwards from the default
# phrase to the nearest $variable.
# --------------------------------------------------
sub _extract_defaults_from_pod {
my ($self, $pod) = @_;
return {} unless $pod;
my %defaults;
# Pattern 1: Default: 'value' or Defaults to: 'value'
while ($pod =~ /(?:Default(?:s? to)?|default(?:s? to)?)[:]\s*([^\n\r]+)/gi) {
my $default_text = $1;
my $match_pos = pos($pod);
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
# Side effects: None.
# --------------------------------------------------
sub _generate_notes {
my ($self, $params) = @_;
my @notes;
foreach my $param (keys %$params) {
my $p = $params->{$param};
unless ($p->{type}) {
push @notes, "$param: type unknown - please review - will set to 'string' as a default";
}
unless (defined $p->{optional}) {
push @notes, "$param: optional status unknown";
# Don't automatically set - let it be undef if we don't know
}
}
return \@notes;
}
# --------------------------------------------------
# _set_defaults
#
# Purpose: Apply default type values to any
# parameters in a schema mode (input
# or output) whose type was not set
# during analysis, setting them to
# 'string' as a conservative fallback.
#
# Entry: $schema - the schema hashref being
# built by _analyze_method.
# $mode - either 'input' or 'output'.
#
# Exit: Returns nothing. Modifies $schema in
# place by setting type => 'string' on
# any parameter that lacks a type, and
# downgrading input confidence to 'low'.
#
# Side effects: Logs type defaulting to stdout when
# verbose is set.
#
# Notes: Called after all analysis is complete
# so that genuine type unknowns can be
# distinguished from analysis gaps.
# --------------------------------------------------
sub _set_defaults {
my ($self, $schema, $mode) = @_;
my $params = $schema->{$mode};
foreach my $param (keys %$params) {
my $p = $params->{$param};
next unless(ref($p) eq 'HASH');
unless ($p->{type}) {
$self->_log(" DEBUG {$mode}{$param}: Setting to 'string' as a default");
$p->{'type'} = 'string';
$schema->{_confidence}{$mode}->{level} = 'low'; # Setting a default means it's a guess
}
}
}
# --------------------------------------------------
# _analyze_relationships
#
# Purpose: Detect inter-parameter relationships
# in a method's source code, including
# mutually exclusive parameters, required
# groups, conditional requirements,
# dependencies, and value-based
# constraints.
#
# Entry: $method - method hashref containing
# at minimum a 'body' key
# with the source string.
#
# Exit: Returns an arrayref of relationship
# hashrefs. Returns an empty arrayref
# if no parameters or no relationships
# are found.
#
# Side effects: Logs detections to stdout when
# verbose is set.
#
# Notes: Parameter names are extracted from
# the my (...) = @_ pattern only â
# shift-style parameters are not
# currently analysed for relationships.
# --------------------------------------------------
sub _analyze_relationships {
my ($self, $method) = @_;
my $code = $method->{body};
my @relationships;
# Extract all parameter names from the method
my @param_names;
if ($code =~ /my\s*\(\s*\$\w+\s*,\s*(.+?)\)\s*=\s*\@_/s) {
my $params = $1;
@param_names = $params =~ /\$(\w+)/g;
}
return [] unless @param_names;
# Detect mutually exclusive parameters
push @relationships, @{$self->_detect_mutually_exclusive($code, \@param_names)};
# Detect required groups (OR logic)
push @relationships, @{$self->_detect_required_groups($code, \@param_names)};
# Detect conditional requirements (IF-THEN)
push @relationships, @{$self->_detect_conditional_requirements($code, \@param_names)};
# Detect dependencies
push @relationships, @{$self->_detect_dependencies($code, \@param_names)};
# Detect value-based constraints
push @relationships, @{$self->_detect_value_constraints($code, \@param_names)};
lib/App/Test/Generator/SchemaExtractor.pm view on Meta::CPAN
my $filename = "$self->{output_dir}/${method_name}.yml";
# Configure YAML::XS to not quote numeric strings
local $YAML::XS::QuoteNumericStrings = 0;
# Extract package name for module field
my $package_name = '';
if ($self->{_document}) {
my $package_stmt = $self->{_document}->find_first('PPI::Statement::Package');
$package_name = $package_stmt ? $package_stmt->namespace : '';
$self->{_package_name} //= $package_name;
}
# Clean up schema for output - use the format expected by App::Test::Generator::Template
my $output = {
function => $method_name,
module => $package_name,
config => {
close_stdin => 0,
dedup => 1,
test_nuls => 0,
test_undef => 0,
test_empty => 1,
test_non_ascii => 0,
test_security => 0
}
};
# Process input parameters with advanced type handling
if($schema->{'input'}) {
if(scalar(keys %{$schema->{'input'}})) {
$output->{'input'} = {};
foreach my $param_name (keys %{$schema->{'input'}}) {
my $param = $schema->{'input'}{$param_name};
if($param->{name}) {
my $name = delete $param->{name};
if($name ne $param_name) {
# Sanity check
croak("BUG: Parameter name - expected $param_name, got $name");
}
}
my $cleaned_param = $self->_serialize_parameter_for_yaml($param);
$output->{'input'}{$param_name} = $cleaned_param;
}
} else {
delete $output->{input};
}
}
# Process output
if($schema->{'output'} && (scalar(keys %{$schema->{'output'}}))) {
if((ref($schema->{output}{_error_handling}) eq 'HASH') && (scalar(keys %{$schema->{output}{_error_handling}}) == 0)) {
delete $schema->{output}{_error_handling};
}
$output->{'output'} = $schema->{'output'};
}
if($schema->{'output'}{'type'} && ($schema->{'output'}{'type'} eq 'scalar')) {
$schema->{'output'}{'type'} = 'string';
$schema->{_confidence}{output}->{level} = 'low'; # A guess
}
# Add 'new' field if object instantiation is needed
if ($schema->{new}) {
# TODO: consider allowing parent class packages up the ISA chain
if(ref($schema->{new}) || ($schema->{new} eq $package_name)) {
$output->{new} = $schema->{new} eq $package_name ? undef : $schema->{'new'};
} else {
$self->_log(" NEW: Don't use $schema->{new} for object insantiation");
delete $schema->{new};
delete $output->{new};
}
}
if(!defined($schema->{_confidence}{input}->{level})) {
$schema->{_confidence}{input} = $self->_calculate_input_confidence($schema->{input});
}
if(!defined($schema->{_confidence}{output}->{level})) {
$schema->{_confidence}{output} = $self->_calculate_output_confidence($schema->{output});
}
# Add relationships if detected
if ($schema->{relationships} && @{$schema->{relationships}}) {
$output->{relationships} = $schema->{relationships};
}
if($schema->{accessor} && scalar(keys %{$schema->{accessor}})) {
$output->{accessor} = $schema->{accessor};
}
open my $fh, '>', $filename;
print $fh YAML::XS::Dump($output);
print $fh $self->_generate_schema_comments($schema, $method_name);
close $fh;
my $rel_info = $schema->{relationships} ?
' [' . scalar(@{$schema->{relationships}}) . ' relationships]' : '';
$self->_log(" Wrote: $filename (input confidence: $schema->{_confidence}{input}->{level})" .
($schema->{new} ? " [requires: $schema->{new}]" : '') . $rel_info);
}
# --------------------------------------------------
# _generate_schema_comments
#
# Purpose: Generate the YAML comment block
# appended to the end of each written
# schema file, containing provenance,
# confidence levels, parameter type
# notes, relationship summaries, and
# warnings about types requiring
# special test setup.
#
# Entry: $schema - the schema hashref as
# built by _analyze_method.
# $method_name - the method name string,
# used in the fuzz
# command hint.
#
# Exit: Returns a string of YAML comment lines
# beginning with a blank line and ending
( run in 0.749 second using v1.01-cache-2.11-cpan-39bf76dae61 )