From 649c883728fd4dcfc830aaa0a19ae82405ff2697 Mon Sep 17 00:00:00 2001 From: Andreas Romeyke <andreas.romeyke@slub-dresden.de> Date: Thu, 16 Feb 2023 11:12:23 +0100 Subject: [PATCH] - added xtra tests --- xt/cleannamespaces.t | 9 ++ xt/compile.t | 29 ++++++ xt/eol.t | 8 ++ xt/fixme.t | 15 ++++ xt/no_nested_exists.t | 71 +++++++++++++++ xt/no_use_v5xxx.t | 25 ++++++ xt/nodiagnostics.t | 24 +++++ xt/portability.t | 48 ++++++++++ xt/unused_subroutines.t | 193 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 422 insertions(+) create mode 100644 xt/cleannamespaces.t create mode 100644 xt/compile.t create mode 100644 xt/eol.t create mode 100644 xt/fixme.t create mode 100644 xt/no_nested_exists.t create mode 100644 xt/no_use_v5xxx.t create mode 100644 xt/nodiagnostics.t create mode 100644 xt/portability.t create mode 100644 xt/unused_subroutines.t diff --git a/xt/cleannamespaces.t b/xt/cleannamespaces.t new file mode 100644 index 0000000..2b8d923 --- /dev/null +++ b/xt/cleannamespaces.t @@ -0,0 +1,9 @@ +use strict; +use warnings; +use Test::More; +plan skip_all => "Test::CleanNamespaces required for testing module namespaces" + unless eval "use Test::CleanNamespaces; 1"; +my @modules = Test::CleanNamespaces->find_modules; +namespaces_clean( @modules ); +done_testing; +1; \ No newline at end of file diff --git a/xt/compile.t b/xt/compile.t new file mode 100644 index 0000000..10d3594 --- /dev/null +++ b/xt/compile.t @@ -0,0 +1,29 @@ +use strict; +use warnings; +use Test::More; +use Perl::Version; +my $required_module_version = '2.3.1'; + + +plan skip_all => "Test::Compile required for testing file compileabilty" + unless eval "use Test::Compile; 1"; +plan skip_all => "Test::Compile version $required_module_version required because fixed prove compatibility" + unless (Perl::Version->new($Test::Compile::VERSION) >= Perl::Version->new($required_module_version)); + +my $test = Test::Compile->new(); +$test->verbose => undef; + + +my @modules = $test->all_pm_files("lib"); +my @binaries = $test->all_pl_files("bin", "example_scripts"); + +my $tests = scalar(@modules) + scalar(@binaries); +plan tests => $tests; +foreach my $file (@modules) { + ok($test->pm_file_compiles($file), "checking file $file"); +} +foreach my $file (@binaries) { + ok($test->pl_file_compiles($file), "checking file $file"); +} + +1; \ No newline at end of file diff --git a/xt/eol.t b/xt/eol.t new file mode 100644 index 0000000..6f0ffdb --- /dev/null +++ b/xt/eol.t @@ -0,0 +1,8 @@ +use strict; +use warnings; +use Test::More; +use Path::Tiny; +plan skip_all => "Test::EOL required for testing filenames portability" + unless eval "use Test::EOL; 1"; +my @whitelist_dirs = path('.')->children( qr/^(?!(cpan_build$|build$|.git$))/ ); +all_perl_files_ok( @whitelist_dirs ); \ No newline at end of file diff --git a/xt/fixme.t b/xt/fixme.t new file mode 100644 index 0000000..a002945 --- /dev/null +++ b/xt/fixme.t @@ -0,0 +1,15 @@ +use strict; +use warnings; +use Test::More; +BEGIN { + plan skip_all => "these tests are for release candidate testing\n(enable it with RELEASE_TESTING=1)" + unless $ENV{RELEASE_TESTING}; +} +plan skip_all => "Test::Fixme required for testing unresolved FIXME or TODO issues" + unless eval "use Test::Fixme; 1"; +run_tests( + where => 'lib', + match => qr/# *(TODO|FIXME|BUG)\b/i, + filename_match => qr/\.(pm|pl)$/ +); +1; \ No newline at end of file diff --git a/xt/no_nested_exists.t b/xt/no_nested_exists.t new file mode 100644 index 0000000..bae0815 --- /dev/null +++ b/xt/no_nested_exists.t @@ -0,0 +1,71 @@ +#search all modules as file paths +use strict; +use warnings; +use File::Find; +use Path::Tiny; +use Test::More; + +my @modules; +sub wanted { + $File::Find::name =~ m#SLUB/LZA/.+\.pm$# && -f $_ && push @modules, $File::Find::name; +} +find(\&wanted, "lib/"); + +# allowed: exists $foo->{$bar} +# allowed: exists $foo->{$bar} and exists $foo->{$bar}->{$baz} +# forbidden: exists $foo->{$bar}->{$baz} +#my $rx_exists=qr{(?!(&&|and) *)(?<=exists)}; +#my $rx_hash_or_hashref=qr/(\$[a-z_]+)((->)?{[^}]*})((->)?{[^}]*})/; +#my $regex_match=qr/$rx_exists $rx_hash_or_hashref/; +use diagnostics; +foreach my $module (@modules) { + my @lines = path($module)->lines; + my $errors = 0; + for (my $i=0; $i<=$#lines; $i++) { + next unless $lines[$i]=~m/exists/; + my $reversed_line = reverse scalar $lines[$i]; + # aus "exists $foo->{bar}->{baz}" + # wird "}zab{>-}rab{>-oof$ stsixe" + # aus "exists $foo->{bar} and exists $foo->{bar}->{baz}" + # wird "}zab{>-}rab{>-oof$ stsixe dna }rab{>-oof$ stsixe" + # damit haben wir mit Trenner ' dna ' folgende Teilstrings: + # "}zab{>-}rab{>-oof$ stsixe" und + # "}rab{>-oof$ stsixe" + # Damit können wir nach Teilstrings suchen + # my $ref = qr/\}[^{]*{/; + # m/($ref>-)($ref>-){1,}[a-z]*\$/ würde auf den Hashref matchen, in $1 hätten wir Treffer für n-ten Hashref-teil, in $2 Treffer für Rest + # was wir fordern, ist, dass lookahead der Rest mit ' dna ' verbunden ist: + # m/($ref>-)($ref>-){1,}([a-z]*\$)(?= dna $2$3)/g + my $rx_ref = qr/\}[^{]*{/; + no warnings; + if ($reversed_line=~m/($rx_ref(?:>-)?)($rx_ref(?:>-)?){1,}([a-z]*\$ stsixe)(?! dna $2$3)/g) { + use warnings; + #print "line=$lines[$i]\n"; + my $lineno = $i + 1; + $errors++; + my $match1 = reverse scalar $1; + my $match2 = reverse scalar $2; + my $match3 = reverse scalar $3; + fail("module $module uses 'exists' on nested hash or hashrefs, but this is dangerous, + because values will be generated, see 'perldoc -f exists' for details. + Here the problematic code: + ---------- snip --------- + $match3$match2$match1 + ---------- snap --------- + (line $lineno) + better use pattern like this: + ---------- snip --------- + $match3$match2 and $match3$match2$match1 + ---------- snap --------- + +"); + + } + } + if ($errors == 0) { + pass("module $module"); + } +} + +done_testing(); +1; diff --git a/xt/no_use_v5xxx.t b/xt/no_use_v5xxx.t new file mode 100644 index 0000000..2b39d71 --- /dev/null +++ b/xt/no_use_v5xxx.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More; +use File::Find; +use Path::Tiny; + + +#search all modules as file paths +my @modules; +sub wanted { + $File::Find::name=~ m#SLUB/LZA/.+\.pm$# && -f $_ && push @modules, $File::Find::name; +} +find(\&wanted, "lib/"); + +foreach my $module (@modules) { + my $content = path( $module )->slurp; + if ($content =~ m/^use v(5\.\d\d);/m) { + fail("module $module has 'use v$1;', you should use 'use feature qw(...);' instead."); + } else { + pass("module $module is fine."); + } +} + +done_testing(); +1; diff --git a/xt/nodiagnostics.t b/xt/nodiagnostics.t new file mode 100644 index 0000000..7aefebf --- /dev/null +++ b/xt/nodiagnostics.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; +use File::Find; +use Path::Tiny; + + +#search all modules as file paths +my @modules; +sub wanted { + $File::Find::name=~ m#SLUB/LZA/.+\.(pm|t)$# && -f $_ && push @modules, $File::Find::name; +} +find(\&wanted, "lib/", "t/"); + +foreach my $module (@modules) { + my $content = path( $module )->slurp; + if ($content =~ m/^use diagnostics;/m) { + fail("module $module has 'use diagnostics;', but should only be used for debugging. It could be enabled via 'perl -Mdiagnostics=-traceonly my_script.pl'"); + } else { + pass("module $module is fine."); + } +} +done_testing(); +1; diff --git a/xt/portability.t b/xt/portability.t new file mode 100644 index 0000000..b35ee1c --- /dev/null +++ b/xt/portability.t @@ -0,0 +1,48 @@ +use strict; +use warnings; +use Test::More; +use Path::Tiny; + +plan skip_all => "Test::Portability::Files required for testing filenames portability" + unless eval "use Test::Portability::Files; 1"; + +#options(all_tests => 1); # to be hyper-strict +options( + #test_mac_length => 1, + test_space => 1, + test_symlink => 1, + test_special_chars => 1, + test_windows_reserved => 1, + test_case => 1, +); +my $manifestfile; +BEGIN{ + # prepare, because we want to check only *.t an *.pm and *.pl files + use File::Find; + my $module_base_dir = path(__FILE__)->parent->parent; + + my $lib_dir = $module_base_dir->child("lib"); + my $t_dir = $module_base_dir->child("t"); + $manifestfile = $module_base_dir->child("MANIFEST")->absolute(); + if ($manifestfile->is_file) { + $manifestfile->remove(); + } + + sub wanted { + m/.*\.(p[ml]|t)$/ && + -f _ && + $manifestfile->append($File::Find::name) && + $manifestfile->append("\n"); + } + find(\&wanted, $lib_dir->stringify, $t_dir->stringify); + +} +# tests +run_tests(); + +#clean up +if ($manifestfile->is_file) { + $manifestfile->remove(); + +} +1; \ No newline at end of file diff --git a/xt/unused_subroutines.t b/xt/unused_subroutines.t new file mode 100644 index 0000000..61d9a4c --- /dev/null +++ b/xt/unused_subroutines.t @@ -0,0 +1,193 @@ +use strict; +use warnings; +use Test::More; +use File::Find; +use Path::Tiny; +use List::Util; + +BEGIN { + plan skip_all => "these tests are for release candidate testing\n(enable it with RELEASE_TESTING=1)" + unless $ENV{RELEASE_TESTING}; +} +# only finds first depth of derived classes! + +#search all modules as file paths +my @modules; +my @binaries; + +# find all Perl-Module paths +sub wantedpm { + $File::Find::name=~ m#SLUB/LZA/.+\.pm$# && -f $_ && push @modules, $File::Find::name; +} + +# find all Perl-Script paths +sub wantedpl { + $File::Find::name=~ m#\.pl$# && -f $_ && push @binaries, $File::Find::name; +} + +sub _find_used_modules { + my $module_or_binary = shift; + my @content = path( $module_or_binary )->lines({chomp=>1}); + my @used_modules; + foreach my $line (@content) { + if ( $line =~ m/use ([A-Za-z0-9:]+)\b/ ) { + push @used_modules, $1; + } elsif ($line =~ m/with ['"]([A-Za-z0-9:]+)["'];/) { + push @used_modules, $1; + } + } + my $contents = join("", @content); + if ($contents =~ m/with qw\s*[(]([A-Za-z0-9: ]+)[)];/m) { + my @found_modules = grep {length($_) > 1} split(/ +/, $1); + push @used_modules, @found_modules; + } + return @used_modules; +} + +# maps Module-Paths to Module-namespace +sub modulepath_to_moduleuse { # maps lib/Foo/Bar/Baz.pm to Foo::Bar::Baz + my $module_path = shift; + my $module = $module_path; + $module =~ s#lib/##; + $module =~ s#\.pm$##; + $module =~ s#/#::#g; + return $module; +} + +# find all subs which are exported by a Module-path +sub get_exported_subs { + my $module_path = shift; + my $module = modulepath_to_moduleuse($module_path); + my $code_string = "use $module; return grep {defined &{\"${module}::\$_\"} } keys \%${module}::;"; + no strict 'refs'; + my @modules = eval "\@${module}::EXPORT;"; + my @exported = grep {$_=~m/^[a-z_]/} eval $code_string; + use strict 'refs'; + if (scalar @exported == 0) { + return @exported + }; + return @modules; +} + +find(\&wantedpm, "lib/"); +find(\&wantedpl, "bin"); + + +my %subs; +# MODULE -> delivers METHOD -> USED BY -> with COUNT +my %required; # special case for moose roles +# ROLE -> sub -> count +my %has_exported; # to check if module has used Export.pm + +# first find all subs +# read each module and search for a sub definition, build hash %subs +foreach my $module (@modules) { + if (List::Util::any {m/use Exporter/} path( $module )->lines({chomp=>1}) ) { + $has_exported{ $module } = 1; + } + my @content = grep {/^\s*sub\s+/} path( $module )->lines({chomp=>1}); + foreach my $line (@content) { + if ($line=~ m/^\s*sub\s+([_A-Za-z0-9:]+)/) { + my $sub = $1; + if (defined $sub && length $sub > 1) { + my $use = modulepath_to_moduleuse($module); + $subs{$use}{$sub}=undef; + } + } + } +} + +# find required (Moose) +foreach my $module (@modules) { + my $content = path( $module )->slurp; + if ( + ($content=~m/use Moose/) && + ($content=~m#requires\s*\(([^\)]*)\)#mx) + ) { + my $requires = $1; + #p($requires); + my @subs = split /,/, $requires; + @subs = grep {defined $_ && length $_ > 0} map { + s/\s*//g; + s/'//g; + $_; + } @subs; + foreach my $require (@subs) { + $required{modulepath_to_moduleuse($module)}{$require}++; + } + } +} +# second find which modules are used +foreach my $module_or_binary (@modules, @binaries) { + # find all used modules + my @used_modules = _find_used_modules( $module_or_binary); + push @used_modules, modulepath_to_moduleuse($module_or_binary); # check itself + my @content = path( $module_or_binary )->lines({chomp=>1}); + foreach my $line (@content) { + foreach my $um (@used_modules) { + foreach my $sub (keys %{$subs{$um}}) { + if ($line =~ m/$sub/) { +# print STDERR "um=$um, sub=$sub, module=$module_or_binary line=$line\n"; + $subs{$um}{$sub}{$module_or_binary}++; # add self used + } + } + } + } +} +# check also if parent obj class uses required +foreach my $module_or_binary (@modules) { + # find all used modules + my @used_modules = _find_used_modules($module_or_binary); + foreach my $parent (@used_modules) { + next unless exists $required{ $parent}; + foreach my $sub (keys %{$required{$parent}}) { + #warn "found parent class $parent of $module_or_binary with sub $sub\n"; + if ($sub =~ m/^_build/) { + # workaround, build methods only visibly used in parent + } + $subs{modulepath_to_moduleuse($module_or_binary)}{$sub}{$module_or_binary}++; + } + } +} +# now print unused modules +foreach my $module (sort @modules) { + my $use = modulepath_to_moduleuse($module); + if ($use =~ m/SLUB::LZA::Rosetta::API$/) { + pass "module '$module', because 1:1 API mapping"; + next; + } + my $res=1; + my @exported_subs = get_exported_subs( $module); + my $is_exported = exists $has_exported{ $module }; + foreach my $sub (sort keys %{$subs{$use}}) { + if ($sub =~ m/DEMOLISH/) { next;} # perlish subroutines + if ($sub =~ m/BUILD/) { next;} # moose-ish subroutines + if ($sub =~ m/get_dotgraph/) {next;} # needed for doc/, make fsm.svg to control FSM in Eventcallback + my $all_used = 0; + # reduce all exported matches + foreach my $where_used_path (sort keys %{ $subs{$use}{$sub} }) { + my $where_used = modulepath_to_moduleuse($where_used_path); + if ($where_used eq $use) { + $all_used--; # reduce by one because self-match 'sub foo…' + my $test_if_exported = $is_exported && (List::Util::any {$_ eq $sub} @exported_subs); + if ($test_if_exported) { + $all_used--; + } # reduce by one because exported via Exporter-module + } + $all_used+= $subs{$use}{$sub}{$where_used_path}; + } + if ($all_used <1) { + $res = undef; + fail "module '$module', subroutine '$sub' unused or module not imported via 'use $use;'"; + #print "-------------------\n"; + #use Data::Printer; p( $subs{$use}{$sub} ); + #print "module '$module' has exported: ", ($is_exported?"yes":"no"), "\n"; + #print "module '$module' has ff. functions (outside callable): \n\t", join("\n\t", @exported_subs), "\n"; + + } + } if ($res) { + pass "module '$module', all subroutines used"; + } +} +done_testing(); +1; -- GitLab