From 9e3eeb656deacda3acf301397b56fb4024e51ae1 Mon Sep 17 00:00:00 2001 From: wickedOne Date: Sat, 8 Jun 2024 10:59:01 +0200 Subject: [PATCH] dependency and segment improvements - added possibility to exclude directories from dependency scanning - added more flexibility to control max number of files per segment --- lib/GPH/PHPStan/Cache.pm | 6 +- lib/GPH/Util/Files.pm | 13 +- lib/GPH/Util/PhpDependencyParser.pm | 48 ++++++-- t/share/Php/Parser/{ => Bar}/BarMapper.php | 0 t/unit/GPH/Util/Files.t | 62 ++++++++++ t/unit/GPH/Util/PhpDependencyParser.t | 134 ++++++++++++++++++--- 6 files changed, 229 insertions(+), 34 deletions(-) rename t/share/Php/Parser/{ => Bar}/BarMapper.php (100%) diff --git a/lib/GPH/PHPStan/Cache.pm b/lib/GPH/PHPStan/Cache.pm index 418b748..438f2cb 100644 --- a/lib/GPH/PHPStan/Cache.pm +++ b/lib/GPH/PHPStan/Cache.pm @@ -3,6 +3,8 @@ package GPH::PHPStan::Cache; use strict; use warnings FATAL => 'all'; +use Cwd; + sub new { my ($class, %args) = @_; @@ -26,7 +28,7 @@ sub parseResultCache { (exists($args{path})) or die "$!"; - open $fh, '<', $args{path} or die "unable to open cache file: $!"; + open $fh, '<', getcwd() . '/' . $args{path} or die "unable to open cache file: $!"; while ($line = <$fh>) { @@ -119,7 +121,7 @@ the C method creates a new GPH::PHPUnit::Config. it takes a hash of options =item path B<(required)> -path to the C file +path to the C file relative to the script execution path. =item depth diff --git a/lib/GPH/Util/Files.pm b/lib/GPH/Util/Files.pm index 6695a93..76f4a6f 100644 --- a/lib/GPH/Util/Files.pm +++ b/lib/GPH/Util/Files.pm @@ -25,16 +25,17 @@ sub segment { push(@{$result{$group}}, $path); } - return(%result) unless defined $args{max}; + return(%result) unless exists($args{max}) or exists($args{segment_max}); foreach $key (keys %result) { + my $max = $args{segment_max}{$key} || $args{max} || 1000; $size = scalar(@{$result{$key}}); - next unless $size > $args{max}; + next unless $size > $max; my $index = 1; - while (scalar(@{$result{$key}}) > $args{max}) { - my @segment = splice @{$result{$key}}, 0, $args{max}; + while (scalar(@{$result{$key}}) > $max) { + my @segment = splice @{$result{$key}}, 0, $max; $result{$key . '.' . $index} = \@segment; $index++; } @@ -85,6 +86,10 @@ the path depth from which to create the segments. defaults to 1. the maximum number of files per segment. +=item segment_max + +a hash defining the max number of files per segment name (e.g. C<< segment_max => {'tests.Unit' => 1000} >>) + =back =back diff --git a/lib/GPH/Util/PhpDependencyParser.pm b/lib/GPH/Util/PhpDependencyParser.pm index f6d3e53..5f18e4a 100644 --- a/lib/GPH/Util/PhpDependencyParser.pm +++ b/lib/GPH/Util/PhpDependencyParser.pm @@ -19,15 +19,29 @@ sub new { } sub dir { - my ($self, $dir, $strip) = @_; + my ($self, %args) = @_; - my @files = File::Find::Rule->file() - ->name('*.php') - ->in($dir) - ; + (exists($args{directories}) && exists($args{strip})) or die "$!"; + + my $rule = File::Find::Rule->new; + + if (exists($args{excludes})) { + $rule->or( + $rule->new->exec(sub { + my ($shortname, $path, $fullname) = @_; + foreach my $exclude (@{$args{excludes}}) { + return 1 if $fullname =~ $exclude; + } + return 0; + })->prune->discard, + $rule->new + ); + } + + my @files = $rule->name('*.php')->in(@{$args{directories}}); foreach my $file (@files) { - $self->parse($file, $strip); + $self->parse($file, $args{strip}); } return ($self); @@ -226,10 +240,26 @@ GPH::Util::PhpDependencyParser - parses one or more php files and builds a depen the C method creates a new GPH::Util::PhpDependencyParser. -=item C<< -Edir($directory, $strip) >> +=item C<< -Edir(%args) >> -scans and builds a dependency map from all php files in C< $directory >. the resulting paths will be stripped of the -prefix defined in C<$strip> +scans and builds a dependency map from all php files in defined directories. the resulting paths will be stripped of the +prefix if defined in the C<$strip> argument. the dir method takes a hash of options, valid option keys include: + +=over + +=item directories B<(required)> + +an array of directory paths (relative to script execution) to scan + +=item strip B<(required)> + +the prefix to strip from the paths + +=item excludes + +an array of directory paths (relative to script execution) to exclude from the scan + +=back =item C<< -Eparse($filepath, $strip) >> diff --git a/t/share/Php/Parser/BarMapper.php b/t/share/Php/Parser/Bar/BarMapper.php similarity index 100% rename from t/share/Php/Parser/BarMapper.php rename to t/share/Php/Parser/Bar/BarMapper.php diff --git a/t/unit/GPH/Util/Files.t b/t/unit/GPH/Util/Files.t index 88d757c..a9f511e 100644 --- a/t/unit/GPH/Util/Files.t +++ b/t/unit/GPH/Util/Files.t @@ -132,6 +132,68 @@ describe "class `$CLASS` segment method" => sub { 'object as expected' ) or diag Dumper(%segments); }; + + tests 'segment with depth 2 and max 1 and segment max' => sub { + my ($object, $exception, $warnings, %segments); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + %segments = $object->segment((paths => @files, depth => 2, max => 1, segment_max => {'tests.Functional' => 2})); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + \%segments, + hash { + field "tests.Unit" => array { + item 'tests/Unit/Parser/MapperTest.php'; + end; + }; + field "tests.Functional" => array { + item 'tests/Functional/Parser/MapperTest.php'; + item 'tests/Functional/Parser/MapperTestCase.php'; + end; + }; + end; + }, + 'object as expected' + ) or diag Dumper(%segments); + }; + + tests 'segment with depth 2, without max and segment max' => sub { + my ($object, $exception, $warnings, %segments); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + %segments = $object->segment((paths => @files, depth => 2, segment_max => {'tests.Unit' => 1})); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + \%segments, + hash { + field "tests.Unit" => array { + item 'tests/Unit/Parser/MapperTest.php'; + end; + }; + field "tests.Functional" => array { + item 'tests/Functional/Parser/MapperTest.php'; + item 'tests/Functional/Parser/MapperTestCase.php'; + end; + }; + end; + }, + 'object as expected' + ) or diag Dumper(%segments); + }; }; done_testing(); diff --git a/t/unit/GPH/Util/PhpDependencyParser.t b/t/unit/GPH/Util/PhpDependencyParser.t index 9974218..4e851e5 100644 --- a/t/unit/GPH/Util/PhpDependencyParser.t +++ b/t/unit/GPH/Util/PhpDependencyParser.t @@ -125,13 +125,21 @@ describe "class `$CLASS` parse method" => sub { }; describe "class `$CLASS` dir method" => sub { + tests 'dies for missing directories argument' => sub { + ok(dies {$CLASS->new()->dir((strip => 't/share/Php/Parser/'))}, 'died with missing directories argument') or note($@); + }; + + tests 'dies for missing strip argument' => sub { + ok(dies {$CLASS->new()->dir((directories => ['t/share/Php/Parser/']))}, 'died with missing directories argument') or note($@); + }; + tests 'parse directory of php files' => sub { my ($object, $exception, $warnings); $exception = dies { $warnings = warns { $object = $CLASS->new(); - $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/'); + $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/')); }; }; @@ -157,7 +165,7 @@ describe "class `$CLASS` dir method" => sub { end; }; field 'App\Tests\Unit\Foo\MapperTrait' => array { - item 'BarMapper.php'; + item 'Bar/BarMapper.php'; end; }; field 'App\Foo\BazMapper' => array { @@ -205,7 +213,7 @@ describe "class `$CLASS` dir method" => sub { }; field classmap => hash { field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; - field 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'Bar/BarMapper.php' => 'App\Foo\BarMapper'; field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; }; @@ -214,6 +222,94 @@ describe "class `$CLASS` dir method" => sub { 'object as expected' ) or diag Dumper($object); }; + + tests 'parse directory of php files and exclude directory' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->dir((directories => ['t/share/Php/Parser/'], excludes => ['t/share/Php/Parser/Bar'], strip => 't/share/Php/Parser/')); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $object, + object { + field usages => hash { + field 'PHPUnit\Framework\TestCase' => array { + end; + }; + field 'App\Foo\BarMapper' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Foo\Mapper' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Foo\QuxMapper' => array { + end; + }; + field 'App\Tests\Unit\Foo\MapperTrait' => array { + end; + }; + field 'App\Foo\BazMapper' => array { + end; + }; + }; + field traits => hash { + field 'App\Tests\Unit\Foo\MapperTrait' => array { + item 'App\Foo\BazMapper'; + end; + }; + }; + field abstracts => hash { + field 'App\Tests\Unit\Foo\MapperTestCase' => array { + item 'App\Foo\QuxMapper'; + item 'App\Foo\Mapper'; + item 'App\Tests\Unit\Foo\MapperTrait'; + item 'PHPUnit\Framework\TestCase'; + end; + }; + }; + field inheritance => hash { + field 'App\Tests\Unit\Foo\MapperTest' => hash { + field 'extends' => 'App\Tests\Unit\Foo\MapperTestCase'; + field 'file' => 'MapperTest.php'; + field 'usages' => array { + item 'App\Foo\Mapper'; + item 'App\Foo\BarMapper'; + end; + }; + end; + }; + field 'App\Tests\Unit\Foo\MapperTestCase' => hash { + field 'extends' => 'PHPUnit\Framework\TestCase'; + field 'file' => 'MapperTestCase.php'; + field 'usages' => array { + item 'App\Foo\QuxMapper'; + item 'App\Foo\Mapper'; + item 'App\Tests\Unit\Foo\MapperTrait'; + item 'PHPUnit\Framework\TestCase'; + end; + }; + end; + }; + }; + field classmap => hash { + field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; + field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; + field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; + }; + end; + }, + 'object as expected' + ) or diag Dumper($object); + }; }; describe "class `$CLASS` inheritance method" => sub { @@ -223,7 +319,7 @@ describe "class `$CLASS` inheritance method" => sub { $exception = dies { $warnings = warns { $object = $CLASS->new(); - $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/')->inheritance(); + $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/'))->inheritance(); }; }; @@ -252,7 +348,7 @@ describe "class `$CLASS` inheritance method" => sub { end; }; field 'App\Tests\Unit\Foo\MapperTrait' => array { - item 'BarMapper.php'; + item 'Bar/BarMapper.php'; item 'MapperTest.php'; end; }; @@ -305,7 +401,7 @@ describe "class `$CLASS` inheritance method" => sub { }; field classmap => hash { field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; - field 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'Bar/BarMapper.php' => 'App\Foo\BarMapper'; field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; end; @@ -324,7 +420,7 @@ describe "class `$CLASS` traits method" => sub { $exception = dies { $warnings = warns { $object = $CLASS->new(); - $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/')) ->traits() ; }; @@ -352,11 +448,11 @@ describe "class `$CLASS` traits method" => sub { end; }; field 'App\Tests\Unit\Foo\MapperTrait' => array { - item 'BarMapper.php'; + item 'Bar/BarMapper.php'; end; }; field 'App\Foo\BazMapper' => array { - item 'BarMapper.php'; + item 'Bar/BarMapper.php'; end; }; end; @@ -404,7 +500,7 @@ describe "class `$CLASS` traits method" => sub { }; field classmap => hash { field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; - field 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'Bar/BarMapper.php' => 'App\Foo\BarMapper'; field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; }; @@ -422,7 +518,7 @@ describe "class `$CLASS` sanitise method" => sub { $exception = dies { $warnings = warns { $object = $CLASS->new(); - $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/')) ->sanitise() ; }; @@ -495,7 +591,7 @@ describe "class `$CLASS` sanitise method" => sub { }; field classmap => hash { field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; - field 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'Bar/BarMapper.php' => 'App\Foo\BarMapper'; field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; end; @@ -511,9 +607,9 @@ describe "class `$CLASS` filter method" => sub { my @filter = qw(App\Foo\BazMapper App\Foo\QuxMapper App\Foo\NonExisting); tests 'dies for missing argument' => sub { - ok(dies {$CLASS->new()->dir('t/share/Php/Parser/', 't/share/Php/Parser/')->filter((collection => \@filter, out => 'namespaces'))}, 'died with missing "in" argument') or note($@); - ok(dies {$CLASS->new()->dir('t/share/Php/Parser/', 't/share/Php/Parser/')->filter((collection => \@filter, in => 'namespaces'))}, 'died with missing "out" argument') or note($@); - ok(dies {$CLASS->new()->dir('t/share/Php/Parser/', 't/share/Php/Parser/')->filter((in => 'namespaces', out => 'files'))}, 'died with missing "collection" argument') or note($@); + ok(dies {$CLASS->new()->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/'))->filter((collection => \@filter, out => 'namespaces'))}, 'died with missing "in" argument') or note($@); + ok(dies {$CLASS->new()->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/'))->filter((collection => \@filter, in => 'namespaces'))}, 'died with missing "out" argument') or note($@); + ok(dies {$CLASS->new()->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/'))->filter((in => 'namespaces', out => 'files'))}, 'died with missing "collection" argument') or note($@); }; tests 'filter parsed dependency map' => sub { @@ -522,7 +618,7 @@ describe "class `$CLASS` filter method" => sub { $exception = dies { $warnings = warns { $object = $CLASS->new(); - @result = $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + @result = $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/')) ->inheritance() ->traits() ->sanitise() @@ -537,7 +633,7 @@ describe "class `$CLASS` filter method" => sub { is( \@result, array { - item 'BarMapper.php'; + item 'Bar/BarMapper.php'; item 'MapperTest.php'; }, 'object as expected' @@ -546,11 +642,11 @@ describe "class `$CLASS` filter method" => sub { tests 'classnames from filtered dependency map' => sub { my ($object, @result, $exception, $warnings); - my @files = qw(BarMapper.php MapperNonExisting.php); + my @files = qw(Bar/BarMapper.php MapperNonExisting.php); $exception = dies { $warnings = warns { $object = $CLASS->new(); - @result = $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + @result = $object->dir((directories => ['t/share/Php/Parser/'], strip => 't/share/Php/Parser/')) ->inheritance() ->traits() ->sanitise()