diff --git a/Makefile.PL b/Makefile.PL index 625f84c..228e149 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -10,10 +10,11 @@ WriteMakefile( VERSION_FROM => 'lib/GPH.pm', LICENSE => 'perl', PREREQ_PM => { - "File::Basename" => 0, - "Time::Piece" => 0, - "XML::LibXML" => 0, - "Cwd" => 0 + "File::Basename" => 0, + "Time::Piece" => 0, + "XML::LibXML" => 0, + "Cwd" => 0, + "File::Find::Rule" => 0, }, CONFIGURE_REQUIRES => { "ExtUtils::MakeMaker" => 0 diff --git a/lib/GPH.pm b/lib/GPH.pm index 593869d..0c67e9a 100644 --- a/lib/GPH.pm +++ b/lib/GPH.pm @@ -3,7 +3,7 @@ package GPH; use strict; use warnings FATAL => 'all'; -our $VERSION = '1.2.1'; +our $VERSION = '1.3.0'; 1; diff --git a/lib/GPH/Composer.pm b/lib/GPH/Composer.pm index 5216dac..b5a22e1 100755 --- a/lib/GPH/Composer.pm +++ b/lib/GPH/Composer.pm @@ -37,9 +37,23 @@ sub match { return 0; } +sub getNamespaces { + my ($self, @paths) = @_; + my (%reversed, @result); + %reversed = reverse %{$self->{classmap}}; + + foreach my $path (@paths) { + $path = '/' . $path if rindex $path, '/', 0; + + next if !defined $reversed{$path}; + push(@result, $reversed{$path}); + } + + return(@result); +}; + sub parseClassMap { my ($self, $path) = @_; - my %classmap = (); open(my $fh, '<', $path) or die "can't open classmap file $!"; @@ -107,6 +121,10 @@ matches a FQCN to the classmap limited by a collection of paths. returns C<1> on returns reference to the parsed classmap hash. +=item C<< -EgetNamespaces(@paths) >> + +returns a list of namespaces for given paths. + =item C<< -EparseClassMap() >> B<(internal)> parses the classmap file with relevant paths (vendor dir is ignored) and stores it in a hash map. diff --git a/lib/GPH/PHPStan/Cache.pm b/lib/GPH/PHPStan/Cache.pm new file mode 100644 index 0000000..418b748 --- /dev/null +++ b/lib/GPH/PHPStan/Cache.pm @@ -0,0 +1,168 @@ +package GPH::PHPStan::Cache; + +use strict; +use warnings FATAL => 'all'; + +sub new { + my ($class, %args) = @_; + + my $self = { + depth => $args{depth} || 1, + relative => $args{relative} || undef, + dependencies => undef, + }; + + bless $self, $class; + + return $self; +} + +sub parseResultCache { + my ($self, %args) = @_; + my ($fh, $line, $key); + my $in_array = 0; + my $in_dependant = 0; + my $in_dependent_files = 0; + + (exists($args{path})) or die "$!"; + + open $fh, '<', $args{path} or die "unable to open cache file: $!"; + + + while ($line = <$fh>) { + chomp $line; + + if ($line =~ /^\s*'dependencies'\s*=>\s*array\s*\($/) { + $in_array = 1; + } elsif ($in_array && $in_dependant == 0 && $line =~ /^\s*'([^']+)'\s*=>\s*$/) { + $key = $self->relative($1); + $in_dependant = 1; + } elsif ($in_array && $in_dependant && $line =~ /^\s*'dependentFiles'\s*=>\s*$/) { + $in_dependent_files = 1; + } elsif ($in_array && $in_dependant && $in_dependent_files && $line =~ /^\s*[0-9]+\s*=>\s*'([^']+)',$/) { + push(@{$self->{dependencies}{$key}}, $self->relative($1)); + } elsif ($in_array && $in_dependant && $in_dependent_files && $line =~ /^\s*\),\s*$/) { + $in_dependent_files = 0; + } elsif ($in_array && $in_dependant && $in_dependent_files == 0 && $line =~ /^\s*\),\s*$/) { + $in_dependant = 0; + } + } + + close($fh); + + return($self); +} + +sub relative { + my ($self, $line) = @_; + + if (!defined $self->{relative}) { + return($line); + } + + return substr $line, index($line, $self->{relative}); +}; + +sub dependencies { + my ($self, @paths) = @_; + my (@unique, @result); + @result = @paths; + + for (my $i = 1; $i <= $self->{depth}; $i++) { + push(@result, $self->iterate(@result)); + } + + @unique = do { my %seen; grep { !$seen{$_}++ } @result }; + + return(@unique); +}; + +sub iterate { + my ($self, @paths) = @_; + my ($path, $dependant, @unique, @result); + @result = @paths; + + foreach $path (@paths) { + for $dependant (@{$self->{dependencies}{$path}}) { + push(@result, $dependant); + } + } + + @unique = do { my %seen; grep { !$seen{$_}++ } @result }; + + return(@unique); +}; + +1; + +__END__ + +=head1 NAME + +GPH::PHPStan::Cache - parse dependencies from phpstan's resultCache.php file. + +=head1 SYNOPSIS + + use GPH::PHPStan::Cache; + + my $cache = GPH::PHPStan::Cache->new((depth => 1, relative => 'src/')); + +=head1 METHODS + +=over 4 + +=item C<< -Enew(%args) >> + +the C method creates a new GPH::PHPUnit::Config. it takes a hash of options, valid option keys include: + +=over + +=item path B<(required)> + +path to the C file + +=item depth + +depth of the dependency scan. defaults to 1. setting it to 2 for instance will retrieve the dependencies of the dependencies as well. + +=item relative + +when you want relative paths, to which directory should they be relative + +=back + +=item C<< -EparseResultCache(%config) >> + +parses the cache file. it takes a hash of options, valid option keys include: + +=over + +=item path B<(required)> + +path to the C file + +=back + +=item C<< -Erelative($line) >> B<(internal)> + +converts file path to relative path + +=item C<< -Edependencies(@paths) >> + +collects all dependencies for given C<@paths> and given C<$depth> + +=item C<< -Eiterate(@paths) >> B<(internal)> + +collects all dependencies for given C<@paths> + +=back + +=head1 AUTHOR + +the GPH::PHPUnit::Config module was written by wicliff wolda + +=head1 COPYRIGHT AND LICENSE + +this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. + +=cut \ No newline at end of file diff --git a/lib/GPH/PHPUnit/Config.pm b/lib/GPH/PHPUnit/Config.pm new file mode 100644 index 0000000..e709262 --- /dev/null +++ b/lib/GPH/PHPUnit/Config.pm @@ -0,0 +1,210 @@ +package GPH::PHPUnit::Config; + +use strict; +use warnings FATAL => 'all'; + +use GPH::Util::XMLHelper; + +sub new { + my ($proto, %args) = @_; + + my $self = bless { + config => undef, + generator => GPH::Util::XMLHelper->new(), + }, $proto; + + my %defaults = ( + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:noNamespaceSchemaLocation' => 'phpunit.xsd', + ); + + my %input = (); + + if (exists($args{attributes}) && ref($args{attributes}) eq 'HASH') { + %input = %{$args{attributes}}; + } + + my %attributes = (%defaults, %input); + + $self->{config} = $self->{generator}->buildElement((name => 'phpunit', attributes => \%attributes)); + + return $self; +} + +sub php { + my ($self, %config) = @_; + my ($key, $attributes, $size); + $size = keys %config; + + if ($size == 0) { + return($self); + } + + my $phpunit = $self->{config}; + + my $php = $self->{generator}->buildElement((name => 'php', parent => $phpunit)); + + foreach $key (keys %config) { + foreach $attributes (@{$config{$key}}) { + $self->{generator}->buildElement((name => $key, attributes => \%{$attributes}, parent => => $php)); + } + } + + return ($self); +} + +sub testsuites { + my ($self, %config) = @_; + my $size = keys %config; + + if ($size == 0) { + return($self); + } + + my $phpunit = $self->{config}; + + my $testsuites = $self->{generator}->buildElement((name => 'testsuites', parent => $phpunit)); + + foreach my $suite (keys %config) { + my $testsuite = $self->{generator}->buildElement((name => 'testsuite', attributes => { 'name' => $suite }, parent => $testsuites)); + + foreach my $path (@{$config{$suite}}) { + next if $path =~ /.*TestCase\.php/; + my $type = ($path =~ /.*\.[a-z]{2,}$/) ? 'file' : 'directory'; + + $self->{generator}->buildElement((name => $type, parent => $testsuite, value => $path)); + } + } + + return $self; +} + +sub extensions { + my ($self, @config) = @_; + my $size = @config; + + if ($size == 0) { + return($self); + } + + my $phpunit = $self->{config}; + + my $extensions = $self->{generator}->buildElement((name => 'extensions', parent => $phpunit)); + + foreach my $extension (@config) { + $self->{generator}->buildElement((name => 'bootstrap', parent => $extensions, attributes => { class => $extension })); + } + + return $self; +} + +sub source { + my ($self, %config) = @_; + my $size = keys %config; + + if ($size == 0) { + return($self); + } + + my $phpunit = $self->{config}; + + my $source = $self->{generator}->buildElement((name => 'source', parent => $phpunit)); + + foreach my $element (keys %config) { + my $sub = $self->{generator}->buildElement((name => $element, parent => $source)); + + foreach my $path (@{$config{$element}}) { + my $type = ($path =~ /.*\.[a-z]{2,}$/) ? 'file' : 'directory'; + + if ($type eq 'file') { + $self->{generator}->buildElement((name => $type, parent => $sub, value => $path)); + } + else { + $self->{generator}->buildElement((name => $type, parent => $sub, value => $path, attributes => { suffix => '.php' })); + } + } + } + + return $self; +} + +sub getConfig { + my $self = shift; + + my $dom = $self->{generator}->getDom(); + + $dom->setDocumentElement($self->{config}); + + return ($dom->toString(1)); +} + +1; + +__END__ + +=head1 NAME + +GPH::PHPUnit::Config - generates phpunit ^10.5 config xml + +=head1 SYNOPSIS + + use GPH::PHPUnit::Config; + + my %attributes = ( + 'bootstrap' => 'tests/bootstrap.php', + 'cacheDirectory' => '.phpunit.cache', + 'xsi:noNamespaceSchemaLocation' => 'https://schema.phpunit.de/10.5/phpunit.xsd', + ); + + my $builder = GPH::Util::PhpDependencyParser->new((attributes => \%attributes)); + + print $builder->getConfig(); + +=head1 METHODS + +=over 4 + +=item C<< -Enew(%args) >> + +the C method creates a new GPH::PHPUnit::Config. it takes a hash of options, valid option keys include: + +=over + +=item attributes + +a hash of attributes to add to the root element of the resulting xml + +=back + +=item C<< -Ephp(%config) >> + +adds php section to the config file. it takes a C<%config> hash containing arrays of hashes. + +=item C<< -Etestsuites(%config) >> + +adds testsuites section to the config file. it takes a C<%config> hash where the keys define the testsuite name and the array value +contains a list of files or directories which should be added to the test suite. + +=item C<< -Eextensions(@extensions) >> + +adds a extensions section to the config file. it takes a C<@extensions> array of extension namespaces to add. + +=item C<< -Esource(%config) >> + +adds a source section to the config file. it takes a C<%config> hash of arrays + +=item C<< -EgetConfig() >> + +returns the xml config file as string. + +=back + +=head1 AUTHOR + +the GPH::Util::PhpDependencyParser module was written by wicliff wolda + +=head1 COPYRIGHT AND LICENSE + +this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. + +=cut \ No newline at end of file diff --git a/lib/GPH/Util/Files.pm b/lib/GPH/Util/Files.pm new file mode 100644 index 0000000..6695a93 --- /dev/null +++ b/lib/GPH/Util/Files.pm @@ -0,0 +1,100 @@ +package GPH::Util::Files; + +use strict; +use warnings FATAL => 'all'; + +sub new { + my ($proto) = @_; + my $self = bless {}, $proto; + return $self; +}; + +sub segment { + my ($self, %args) = @_; + my (@arr, $group, %result, $depth, $path, $key, $size); + + (exists($args{paths})) or die "$!"; + $depth = $args{depth} || 1; + + + foreach $path (@{$args{paths}}) { + @arr = split(/\//, $path, $depth + 1); + $group = join('.', @arr[0..$depth - 1]); + + $result{$group} = [] unless defined($result{$group}); + push(@{$result{$group}}, $path); + } + + return(%result) unless defined $args{max}; + + foreach $key (keys %result) { + $size = scalar(@{$result{$key}}); + next unless $size > $args{max}; + + my $index = 1; + + while (scalar(@{$result{$key}}) > $args{max}) { + my @segment = splice @{$result{$key}}, 0, $args{max}; + $result{$key . '.' . $index} = \@segment; + $index++; + } + } + + return(%result); +}; + +1; + +__END__ + +=head1 NAME + +GPH::Util::Files - file related util methods + +=head1 SYNOPSIS + + use GPH::Util::Files; + + my $util = GPH::Util::Files->new(); + + $util->segment(wq(Foo/Bar/baz.php Foo/Qux/Fred.php)); + +=head1 METHODS + +=over 4 + +=item C<< -Enew() >> + +the C method returns a new GPH::Util::Files object. + +=item C<< -Esegment(%args) >> + +return an hash of segments based on given file path collection. the segment method takes a hash of options, valid option keys include: + +=over + +=item paths B<(required)> + +file paths to segment. + +=item depth + +the path depth from which to create the segments. defaults to 1. + +=item max + +the maximum number of files per segment. + +=back + +=back + +=head1 AUTHOR + +the GPH::Util::Files module was written by wicliff wolda + +=head1 COPYRIGHT AND LICENSE + +this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. + +=cut \ No newline at end of file diff --git a/lib/GPH/Util/Php.pm b/lib/GPH/Util/Php.pm index 80879c8..2468ca0 100644 --- a/lib/GPH/Util/Php.pm +++ b/lib/GPH/Util/Php.pm @@ -4,7 +4,6 @@ use strict; use warnings FATAL => 'all'; use Cwd; -use Data::Dumper; sub new { my ($proto) = @_; diff --git a/lib/GPH/Util/PhpDependencyParser.pm b/lib/GPH/Util/PhpDependencyParser.pm new file mode 100644 index 0000000..0d00704 --- /dev/null +++ b/lib/GPH/Util/PhpDependencyParser.pm @@ -0,0 +1,287 @@ +package GPH::Util::PhpDependencyParser; + +use strict; +use warnings FATAL => 'all'; + +use File::Find::Rule; +use Cwd; + +sub new { + my ($proto) = @_; + + return bless { + 'usages' => {}, + 'traits' => {}, + 'abstracts' => {}, + 'inheritance' => {}, + 'classmap' => {}, + }, $proto; +} + +sub dir { + my ($self, $dir, $strip) = @_; + + my @files = File::Find::Rule->file() + ->name('*.php') + ->in($dir) + ; + + foreach my $file (@files) { + $self->parse($file, $strip); + } + + return ($self); +} + +sub parse { + my ($self, $path, $strip) = @_; + my ($fh, $class, $namespace, @usages, %aliases, $fqcn); + + return $self unless $path =~ '[/]{0,}([^/]+)\.php$'; + + open($fh, '<', $path) or die "unable to open file $path : $!"; + + $class = $1; + + $path =~ s/$strip//g; + + while (<$fh>) { + chomp $_; + + next if $_ =~ /^[\* ]\*/ or $_ eq ''; + + # collect usages + if ($_ =~ /^use ([^ ;]+).*$/) { + next if $_ =~ /^use function .*$/; + + $self->{usages}{$1} = [] unless defined $self->{usages}{$1}; + push(@usages, $1); + + # register aliases + if ($_ =~ /^use ([^ ]+) as ([^;]+);/) { + $aliases{$2} = $1; + } + + next; + } + + # get namespace + if ($_ =~ /^namespace (.*);$/) { + $namespace = $1; + + next; + } + + # process inheritance + if ($_ =~ /$class\s*extends\s*([^ ]+)/) { + my $meta = quotemeta($1); + my @matches = grep(/$meta/, @usages); + + if (defined $aliases{$1}) { + $fqcn = $aliases{$1}; + } + elsif (defined $matches[0]) { + $fqcn = $matches[0]; + } + else { + $fqcn = $namespace . '\\' . $1; + } + + $self->{inheritance}{$namespace . '\\' . $class} = { + extends => $fqcn, + usages => \@usages, + file => $path, + }; + } + + # define class type (class, trait, enum, etc.) + if ($_ =~ "[ ]{0,}([^ ]+) $class(?:[ :]|\$){1,}") { + $fqcn = $namespace . '\\' . $class; + $self->{classmap}{$path} = $fqcn; + + if ($_ =~ "^abstract class $class" || $class =~ /.*TestCase$/) { + $self->{abstracts}{$fqcn} = \@usages; + } + elsif (defined $1 && $1 eq 'trait') { + $self->{traits}{$fqcn} = \@usages; + } + else { + foreach my $dep (@usages) { + push(@{$self->{usages}{$dep}}, $path); + } + } + + last; + } + } + + close($fh); + + return ($self); +} + +sub inheritance { + my ($self) = @_; + + foreach my $class (keys %{$self->{inheritance}}) { + my $parent = $self->{inheritance}{$class}{extends}; + + while (defined $self->{inheritance}{$parent}) { + foreach my $dependency (@{$self->{inheritance}{$parent}{usages}}) { + push(@{$self->{usages}{$dependency}}, $self->{inheritance}{$class}{file}); + } + + $parent = $self->{inheritance}{$parent}{extends}; + } + } + return ($self); +} + +sub traits { + my ($self) = @_; + + foreach my $trait (keys %{$self->{traits}}) { + next unless defined $self->{usages}{$trait}; + + foreach my $path (@{$self->{usages}{$trait}}) { + foreach my $dependency (@{$self->{traits}{$trait}}) { + push(@{$self->{usages}{$dependency}}, $path); + } + } + } + + return ($self); +} + +sub sanitise { + my ($self) = @_; + my $key; + + foreach $key (keys $self->{abstracts}) { + delete $self->{usages}{$key}; + } + foreach $key (keys $self->{traits}) { + delete $self->{usages}{$key}; + } + + return ($self); +} + +sub filter { + my ($self, %args) = @_; + my (@result, @namespaces, @out); + + (exists($args{collection}) && exists($args{in}) && exists($args{out})) or die "$!"; + + @namespaces = ($args{in} eq 'namespaces') ? @{$args{collection}} : $self->namespaces(@{$args{collection}}); + + foreach my $namespace (@namespaces) { + push(@result, @{$self->{usages}{$namespace}}) unless !defined $self->{usages}{$namespace}; + } + + my @unique = do { + my %seen; + grep {!$seen{$_}++} @result + }; + + @out = ($args{out} eq 'files') ? @unique : $self->namespaces(@unique); + + return (sort @out); +} + +sub namespaces { + my ($self, @files) = @_; + my @result; + + foreach my $filename (@files) { + push(@result, $self->{classmap}{$filename}) unless !defined $self->{classmap}{$filename}; + } + + return(@result); +} + +1; + +__END__ + +=head1 NAME + +GPH::Util::PhpDependencyParser - parses one or more php files and builds a dependency map + +=head1 SYNOPSIS + + use GPH::Util::PhpDependencyParser; + + my $config = GPH::PHPUnit::Config->new() + ->dir('/Users/foo/Code/bar/tests/', '/Users/foo/Code/bar/') + ; + + print $parser->filter(['App\Service\Provider\FooProvider.php'); + +=head1 METHODS + +=over 4 + +=item C<< -Enew() >> + +the C method creates a new GPH::Util::PhpDependencyParser. + +=item C<< -Edir($directory, $strip) >> + +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> + +=item C<< -Eparse($filepath, $strip) >> + +scans and builds a dependency map the php file defined in C<$filepath>. the resulting paths will be stripped of the +prefix defined in C<$strip> + +returns reference to the parsed classmap hash. + +=item C<< -Einheritance() >> + +enriches the dependency map by processing the php dependency graph. + +=item C<< -Etraits() >> + +enriches the dependency map by processing traits. + +=item C<< -Esanitise() >> + +removes abstract classes and traits from the dependency map. + +=item C<< -Efilter(%args) >> + +returns an array of file paths or namespaces which have a dependency on given collection. it takes a hash of options, valid option keys include: + +=over + +=item collection B<(required)> + +an array of either file paths or namespaces for which you want dependencies + +=item in B<(required)> + +type of collection provided. valid values are "namespaces" and "files" + +=item out B<(required)> + +type of collection required. valid values are "namespaces" and "files" + +=back + +=item C<< -Enamespaces(@files) >> B<(internal)> + +returns an array of namespaces for given C<@files>. + +=back + +=head1 AUTHOR + +the GPH::Util::PhpDependencyParser module was written by wicliff wolda + +=head1 COPYRIGHT AND LICENSE + +this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. + +=cut \ No newline at end of file diff --git a/t/share/PHPStan/resultCache.php b/t/share/PHPStan/resultCache.php new file mode 100644 index 0000000..6d496b8 --- /dev/null +++ b/t/share/PHPStan/resultCache.php @@ -0,0 +1,40 @@ + 1714381667, + 'meta' => array(), + 'projectExtensionFiles' => array(), + 'errorsCallback' => array(), + 'collectedDataCallback' => array(), + 'dependencies' => array( + '/builds/phrase-tag-bundle/src/Foo/Bar.php' => + array ( + 'fileHash' => '2530220185e341a0cbca2f061d261d1630e67c20', + 'dependentFiles' => + array ( + 0 => '/builds/phrase-tag-bundle/src/Foo/Baz.php', + ), + ), + '/builds/phrase-tag-bundle/src/Foo/Baz.php' => + array ( + 'fileHash' => '2530220185e341a0cbca2f061d261d1630e67c20', + 'dependentFiles' => + array ( + 0 => '/builds/phrase-tag-bundle/src/Foo/Qux.php', + 1 => '/builds/phrase-tag-bundle/src/Foo/Corge.php', + ), + ), + '/builds/phrase-tag-bundle/src/Foo/Qux.php' => + array ( + 'fileHash' => '2530220185e341a0cbca2f061d261d1630e67c20', + 'dependentFiles' => + array (), + ), + '/builds/phrase-tag-bundle/src/Foo/Corge.php' => + array ( + 'fileHash' => '2530220185e341a0cbca2f061d261d1630e67c20', + 'dependentFiles' => + array (), + ), + ), + 'exportedNodesCallback' => array(), +]; \ No newline at end of file diff --git a/t/share/Php/Parser/BarMapper.php b/t/share/Php/Parser/BarMapper.php new file mode 100644 index 0000000..9d41cd0 --- /dev/null +++ b/t/share/Php/Parser/BarMapper.php @@ -0,0 +1,11 @@ + + */ +class MapperTest extends MapperTestCase +{ +} \ No newline at end of file diff --git a/t/share/Php/Parser/MapperTestCase.php b/t/share/Php/Parser/MapperTestCase.php new file mode 100644 index 0000000..35a1b0a --- /dev/null +++ b/t/share/Php/Parser/MapperTestCase.php @@ -0,0 +1,22 @@ + + */ +abstract class MapperTestCase extends BaseTestCase +{ + +} \ No newline at end of file diff --git a/t/share/Php/Parser/MapperTrait.php b/t/share/Php/Parser/MapperTrait.php new file mode 100644 index 0000000..9a3feae --- /dev/null +++ b/t/share/Php/Parser/MapperTrait.php @@ -0,0 +1,16 @@ + sub { }; }; +describe 'test get namespaces' => sub { + tests 'namespaces' => sub { + my ($object, $exception, $warnings, @result); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new((classmap => CLASSMAP_FILE)); + @result = $object->getNamespaces(qw(/src/Command/PhraseKeyTagCommand.php src/Command/PhraseKeyUntagCommand.php src/NonExisting.php)); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + is(\@result, array { + item "WickedOne\\PhraseTagBundle\\Command\\PhraseKeyTagCommand"; + item "WickedOne\\PhraseTagBundle\\Command\\PhraseKeyUntagCommand"; + }, 'expected namespaces returned') or diag Dumper(@result); + }; +}; + done_testing(); diff --git a/t/unit/GPH/PHPMD.t b/t/unit/GPH/PHPMD.t index 92d4f6a..dc3e6a4 100644 --- a/t/unit/GPH/PHPMD.t +++ b/t/unit/GPH/PHPMD.t @@ -18,7 +18,7 @@ describe "class `$CLASS`" => sub { ok(lives {$CLASS->new((owner => '@teams/alpha', cyclo_level => 8))}, 'lived with mandatory options') or note($@); }; - tests 'instantation' => sub { + tests 'instantiation' => sub { my ($object, $exception, $warnings); $exception = dies { diff --git a/t/unit/GPH/PHPStan/Cache.t b/t/unit/GPH/PHPStan/Cache.t new file mode 100644 index 0000000..aef4217 --- /dev/null +++ b/t/unit/GPH/PHPStan/Cache.t @@ -0,0 +1,196 @@ +#!/usr/bin/perl +package t::unit::GPH::PHPStan::Cache; + +use strict; +use warnings; + +use Test2::V0 -target => 'GPH::PHPStan::Cache'; +use Test2::Tools::Spec; + +use Data::Dumper; + +describe "class `$CLASS`" => sub { + tests 'it can be instantiated' => sub { + can_ok($CLASS, 'new'); + }; + + tests 'instantiation without arguments' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + $object, + object { + field depth => 1; + field relative => undef; + field dependencies => undef; + }, + 'object as expected' + ) or diag Dumper($object); + }; + + tests 'instantiation with arguments' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new((depth => 2, relative => 'build/')); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + $object, + object { + field depth => 2; + field relative => 'build/'; + field dependencies => undef; + }, + 'object as expected' + ) or diag Dumper($object); + }; +}; + +describe "class `$CLASS` parseResultCache method" => sub { + tests 'dies for missing path argument' => sub { + ok(dies {$CLASS->new()->parseResultCache()}, 'missing path argument') or note($@); + }; + + tests 'dies for incorrect path argument' => sub { + ok(dies {$CLASS->new()->parseResultCache((path => 'foo/bar/baz.php'))}, 'missing path argument') or note($@); + }; + + tests 'test parseResultCache without constructor arguments' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new()->parseResultCache((path => 't/share/PHPStan/resultCache.php')); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + $object, + object { + field depth => 1; + field relative => undef; + field dependencies => hash { + field '/builds/phrase-tag-bundle/src/Foo/Baz.php' => array { + item '/builds/phrase-tag-bundle/src/Foo/Qux.php'; + item '/builds/phrase-tag-bundle/src/Foo/Corge.php'; + end; + }; + field '/builds/phrase-tag-bundle/src/Foo/Bar.php' => array { + item '/builds/phrase-tag-bundle/src/Foo/Baz.php'; + end; + }; + }; + }, + 'object as expected' + ) or diag Dumper($object); + }; + + tests 'test parseResultCache with constructor arguments' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new((depth => 2, relative => 'src/')) + ->parseResultCache((path => 't/share/PHPStan/resultCache.php')); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + $object, + object { + field depth => 2; + field relative => 'src/'; + field dependencies => hash { + field 'src/Foo/Baz.php' => array { + item 'src/Foo/Qux.php'; + item 'src/Foo/Corge.php'; + end; + }; + field 'src/Foo/Bar.php' => array { + item 'src/Foo/Baz.php'; + end; + }; + }; + }, + 'object as expected' + ) or diag Dumper($object); + }; +}; + +describe "class `$CLASS` dependencies method" => sub { + tests 'test without constructor arguments' => sub { + my (@result, $exception, $warnings); + + $exception = dies { + $warnings = warns { + @result = $CLASS->new() + ->parseResultCache((path => 't/share/PHPStan/resultCache.php')) + ->dependencies(qw(/builds/phrase-tag-bundle/src/Foo/Bar.php)) + ; + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + \@result, + array { + item '/builds/phrase-tag-bundle/src/Foo/Bar.php'; + item '/builds/phrase-tag-bundle/src/Foo/Baz.php'; + end; + }, 'result as expected' + ) or diag Dumper(@result); + }; + + tests 'test with constructor arguments' => sub { + my (@result, $exception, $warnings); + + $exception = dies { + $warnings = warns { + @result = $CLASS->new((depth => 2, relative => 'src/')) + ->parseResultCache((path => 't/share/PHPStan/resultCache.php')) + ->dependencies(qw(src/Foo/Bar.php)) + ; + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is ( + \@result, + array { + item 'src/Foo/Bar.php'; + item 'src/Foo/Baz.php'; + item 'src/Foo/Qux.php'; + item 'src/Foo/Corge.php'; + end; + }, 'result as expected' + ) or diag Dumper(@result); + }; +}; + +done_testing(); + diff --git a/t/unit/GPH/PHPUnit.t b/t/unit/GPH/PHPUnit.t index 0590246..854beca 100644 --- a/t/unit/GPH/PHPUnit.t +++ b/t/unit/GPH/PHPUnit.t @@ -62,7 +62,7 @@ describe 'configuration options' => sub { @expected_baseline = qw{/src/Service/Provider/ /src/Mapper/}; }; - tests 'instantation' => sub { + tests 'instantiation' => sub { my ($object, $exception, $warnings); $exception = dies { diff --git a/t/unit/GPH/PHPUnit/Config.t b/t/unit/GPH/PHPUnit/Config.t new file mode 100644 index 0000000..7b0caca --- /dev/null +++ b/t/unit/GPH/PHPUnit/Config.t @@ -0,0 +1,352 @@ +#!/usr/bin/perl +package t::unit::GPH::PHPUnit::Config; + +use strict; +use warnings; + +use Test2::V0 -target => 'GPH::PHPUnit::Config'; +use Test2::Tools::Spec; + +use Data::Dumper; + +describe "class `$CLASS`" => sub { + tests 'it can be instantiated' => sub { + can_ok($CLASS, 'new'); + }; + + tests 'instantiation with wrong config' => sub { + my ($config, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $config = $CLASS->new((attributes => []))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, ' + +', 'object as expected' + ) or diag Dumper($config); + }; + + tests 'instantiation without config' => sub { + my ($config, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, ' + +', 'object as expected' + ) or diag Dumper($config); + }; + + tests 'instantiation with config' => sub { + my ($config, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $config = $CLASS->new((attributes => { 'bootstrap' => 'tests/bootstrap.php' }))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, ' + +', 'object as expected' + ) or diag Dumper($config); + }; + + tests 'instantiation with config override' => sub { + my ($config, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $config = $CLASS->new((attributes => { 'xsi:noNamespaceSchemaLocation' => 'https://schema.phpunit.de/10.5/phpunit.xsd' }))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, ' + +', 'object as expected' + ) or diag Dumper($config); + }; +}; + +describe "class `$CLASS` php method" => sub { + tests 'php method without config' => sub { + my ($config, $exception, $warnings); + my $result = ' + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->php()->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + + tests 'php method with config' => sub { + my ($config, $exception, $warnings); + my $result = ' + + + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->php((server => [{ name => 'SHELL_VERBOSITY', value => '-1' }]))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; +}; + +describe "class `$CLASS` testsuites method" => sub { + tests 'testsuites method without config' => sub { + my ($config, $exception, $warnings); + my $result = ' + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->testsuites()->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + + tests 'testsuites method with file config' => sub { + my ($config, $exception, $warnings); + my %testsuites = ( + 'tests.Unit' => ['tests/Unit/MapperTest.php'], + ); + my $result = ' + + + + tests/Unit/MapperTest.php + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->testsuites(%testsuites)->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + + tests 'testsuites method with dir config' => sub { + my ($config, $exception, $warnings); + my %testsuites = ( + 'tests.Functional' => [ + 'tests/Functional/MapperTestCase.php', + 'tests/Functional/BarMapper', + ], + ); + my $result = ' + + + + tests/Functional/BarMapper + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->testsuites(%testsuites)->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; +}; + +describe "class `$CLASS` extensions method" => sub { + tests 'extension method without config' => sub { + my ($config, $exception, $warnings); + my $result = ' + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->extensions()->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + + tests 'extension method with config' => sub { + my ($config, $exception, $warnings); + my $result = ' + + + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->extensions(qw(DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; +}; + +describe "class `$CLASS` source method" => sub { + tests 'source method without config' => sub { + my ($config, $exception, $warnings); + my $result = ' + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->source()->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + + tests 'source method with directory config' => sub { + my ($config, $exception, $warnings); + my $result = ' + + + + src + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->source(( + include => [ + 'src' + ], + ))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; + +tests 'source method with file config' => sub { + my ($config, $exception, $warnings); + my $result = ' + + + + ./src/Kernel.php + + + +'; + + $exception = dies { + $warnings = warns { + $config = $CLASS->new()->source(( + exclude => [ + './src/Kernel.php', + ] + ))->getConfig(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $config, $result, 'object as expected' + ) or diag Dumper($config . "\n\n\n" . $result); + }; +}; + +done_testing(); + diff --git a/t/unit/GPH/PHPUnit/Stats.t b/t/unit/GPH/PHPUnit/Stats.t index 5b25c00..81a3d7c 100644 --- a/t/unit/GPH/PHPUnit/Stats.t +++ b/t/unit/GPH/PHPUnit/Stats.t @@ -26,7 +26,7 @@ describe "class `$CLASS`" => sub { }; describe 'configuration options' => sub { - tests 'instantation' => sub { + tests 'instantiation' => sub { my ($object, $exception, $warnings); $exception = dies { diff --git a/t/unit/GPH/Psalm.t b/t/unit/GPH/Psalm.t index 6faed28..9da5278 100644 --- a/t/unit/GPH/Psalm.t +++ b/t/unit/GPH/Psalm.t @@ -75,7 +75,7 @@ describe 'configuration options' => sub { $expected_plugins = \@plugins; }; - tests 'instantation' => sub { + tests 'instantiation' => sub { my ($object, $exception, $warnings); $exception = dies { diff --git a/t/unit/GPH/Util/Files.t b/t/unit/GPH/Util/Files.t new file mode 100644 index 0000000..88d757c --- /dev/null +++ b/t/unit/GPH/Util/Files.t @@ -0,0 +1,138 @@ +#!/usr/bin/perl +package t::unit::GPH::Util::Files; + +use strict; +use warnings; + +use Test2::V0 -target => 'GPH::Util::Files'; +use Test2::Tools::Spec; + +use Data::Dumper; + +describe "class `$CLASS`" => sub { + tests 'it can be instantiated' => sub { + can_ok($CLASS, 'new'); + }; + + tests 'instantiation' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + }; +}; + +describe "class `$CLASS` segment method" => sub { + my @files = [ + 'tests/Unit/Parser/MapperTest.php', + 'tests/Functional/Parser/MapperTest.php', + 'tests/Functional/Parser/MapperTestCase.php', + ]; + + tests 'dies for with missing paths' => sub { + ok(dies {$CLASS->new()->segment((max => 10, depth => 2))}, 'died with non existing directory') or note($@); + }; + + tests 'segment with default args' => sub { + my ($object, $exception, $warnings, %segments); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + %segments = $object->segment((paths => @files)); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + \%segments, + hash { + field tests => array { + item 'tests/Unit/Parser/MapperTest.php'; + 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' => sub { + my ($object, $exception, $warnings, %segments); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + %segments = $object->segment((paths => @files, depth => 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 and max 1' => sub { + my ($object, $exception, $warnings, %segments); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + %segments = $object->segment((paths => @files, depth => 2, max => 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/MapperTestCase.php'; + end; + }; + end; + field "tests.Functional.1" => array { + item 'tests/Functional/Parser/MapperTest.php'; + end; + }; + end; + }, + 'object as expected' + ) or diag Dumper(%segments); + }; +}; + +done_testing(); + diff --git a/t/unit/GPH/Util/Php.t b/t/unit/GPH/Util/Php.t index 772ed97..f294fa4 100644 --- a/t/unit/GPH/Util/Php.t +++ b/t/unit/GPH/Util/Php.t @@ -16,7 +16,7 @@ describe "class `$CLASS`" => sub { can_ok($CLASS, 'new'); }; - tests 'instantation' => sub { + tests 'instantiation' => sub { my ($object, $exception, $warnings); $exception = dies { diff --git a/t/unit/GPH/Util/PhpDependencyParser.t b/t/unit/GPH/Util/PhpDependencyParser.t new file mode 100644 index 0000000..9974218 --- /dev/null +++ b/t/unit/GPH/Util/PhpDependencyParser.t @@ -0,0 +1,576 @@ +#!/usr/bin/perl +package t::unit::GPH::Util::PhpDependencyParser; + +use strict; +use warnings; + +use Test2::V0 -target => 'GPH::Util::PhpDependencyParser'; +use Test2::Tools::Spec; + +use Data::Dumper; + +describe "class `$CLASS`" => sub { + tests 'it can be instantiated' => sub { + can_ok($CLASS, 'new'); + }; + + tests 'instantiation' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $object, + object { + field usages => {}; + field traits => {}; + field abstracts => {}; + field inheritance => {}; + field classmap => {}; + end; + }, + 'object as expected' + ) or diag Dumper($object); + }; +}; + +describe "class `$CLASS` parse method" => sub { + tests 'dies for non existing directory' => sub { + ok(dies {$CLASS->new()->parse('foo/bar/baz.php')}, 'died with non existing directory') or note($@); + }; + + tests 'parse non php file' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->parse('t/share/Php/textfile.txt'); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $object, + object { + field usages => {}; + field traits => {}; + field abstracts => {}; + field inheritance => {}; + field classmap => {}; + end; + }, + 'object as expected' + ) or diag Dumper($object); + }; + + tests 'parse php file' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->parse('t/share/Php/Parser/MapperTest.php', 't/share/Php/Parser/'); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $object, + object { + field usages => hash { + field 'App\Foo\Mapper' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Foo\BarMapper' => array { + item 'MapperTest.php'; + end; + }; + }; + field traits => {}; + field abstracts => {}; + 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; + }; + end; + }; + field classmap => hash { + field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; + end; + }; + end; + }, 'object as expected' + ) or diag Dumper($object); + }; +}; + +describe "class `$CLASS` dir method" => sub { + 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/'); + }; + }; + + 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 { + item 'BarMapper.php'; + 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 'BarMapper.php' => 'App\Foo\BarMapper'; + 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 { + tests 'parse directory of php files and apply inheritance' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/')->inheritance(); + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + $object, + object { + field usages => hash { + field 'PHPUnit\Framework\TestCase' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Foo\BarMapper' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Foo\Mapper' => array { + item 'MapperTest.php'; + item 'MapperTest.php'; + end; + }; + field 'App\Foo\QuxMapper' => array { + item 'MapperTest.php'; + end; + }; + field 'App\Tests\Unit\Foo\MapperTrait' => array { + item 'BarMapper.php'; + item 'MapperTest.php'; + end; + }; + field 'App\Foo\BazMapper' => array { + end; + }; + end; + }; + field traits => hash { + field 'App\Tests\Unit\Foo\MapperTrait' => array { + item 'App\Foo\BazMapper'; + end; + }; + 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; + }; + 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; + }; + end; + }; + field classmap => hash { + field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; + field 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; + field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; + end; + }; + end; + }, + 'object as expected' + ) or diag Dumper($object); + }; +}; + +describe "class `$CLASS` traits method" => sub { + tests 'parse directory of php files and apply traits' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + ->traits() + ; + }; + }; + + 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 { + item 'BarMapper.php'; + end; + }; + field 'App\Foo\BazMapper' => array { + item 'BarMapper.php'; + end; + }; + end; + }; + field traits => hash { + field 'App\Tests\Unit\Foo\MapperTrait' => array { + item 'App\Foo\BazMapper'; + end; + }; + 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; + }; + 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; + }; + }; + 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; + }; + end; + }; + field classmap => hash { + field 'MapperTestCase.php' => 'App\Tests\Unit\Foo\MapperTestCase'; + field 'BarMapper.php' => 'App\Foo\BarMapper'; + 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` sanitise method" => sub { + tests 'parse directory of php files and sanitise' => sub { + my ($object, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + ->sanitise() + ; + }; + }; + + 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\Foo\BazMapper' => array { + end; + }; + end; + }; + field traits => hash { + field 'App\Tests\Unit\Foo\MapperTrait' => array { + item 'App\Foo\BazMapper'; + end; + }; + 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; + }; + }; + 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 'BarMapper.php' => 'App\Foo\BarMapper'; + field 'MapperTrait.php' => 'App\Tests\Unit\Foo\MapperTrait'; + field 'MapperTest.php' => 'App\Tests\Unit\Foo\MapperTest'; + end; + }; + end; + }, + 'object as expected' + ) or diag Dumper($object); + }; +}; + +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($@); + }; + + tests 'filter parsed dependency map' => sub { + my ($object, @result, $exception, $warnings); + + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + @result = $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + ->inheritance() + ->traits() + ->sanitise() + ->filter((collection => \@filter, in => 'namespaces', out => 'files')) + ; + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + \@result, + array { + item 'BarMapper.php'; + item 'MapperTest.php'; + }, + 'object as expected' + ) or diag Dumper(@result); + }; + + tests 'classnames from filtered dependency map' => sub { + my ($object, @result, $exception, $warnings); + my @files = qw(BarMapper.php MapperNonExisting.php); + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + @result = $object->dir('t/share/Php/Parser/', 't/share/Php/Parser/') + ->inheritance() + ->traits() + ->sanitise() + ->filter((collection => \@files, in => 'files', out => 'namespaces')) + ; + }; + }; + + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); + + is( + \@result, + array { + item 'App\Tests\Unit\Foo\MapperTest'; + end; + }, + 'object as expected' + ) or diag Dumper(@result); + }; +}; + +done_testing();