diff --git a/lib/GPH.pm b/lib/GPH.pm
index 1555e5f..f0f4fab 100644
--- a/lib/GPH.pm
+++ b/lib/GPH.pm
@@ -3,7 +3,7 @@ package GPH;
use strict;
use warnings FATAL => 'all';
-our $VERSION = '1.4.0';
+our $VERSION = '1.4.1';
1;
@@ -55,7 +55,7 @@ generate custom configuration file for L
=head1 VERSION
-1.4.0
+1.4.1
=head1 AUTHOR
diff --git a/lib/GPH/PHPUnit/Teardown.pm b/lib/GPH/PHPUnit/Teardown.pm
new file mode 100644
index 0000000..e61c51b
--- /dev/null
+++ b/lib/GPH/PHPUnit/Teardown.pm
@@ -0,0 +1,178 @@
+package GPH::PHPUnit::Teardown;
+
+use strict;
+use warnings FATAL => 'all';
+
+use GPH::PHPUnit::Teared;
+
+sub new {
+ my ($proto, %args) = @_;
+
+ exists($args{files}) or die "file must be defined: $!";
+
+ my $self = bless {
+ files => $args{files},
+ debug => $args{debug} // 0,
+ strict => $args{strict} // 0,
+ teared => {},
+ }, $proto;
+
+ return ($self);
+}
+
+sub parse {
+ my ($self) = @_;
+ my ($fh);
+
+ foreach my $file (@{$self->{files}}) {
+ next unless $file =~ /Test|TestCase$/;
+
+ open($fh, '<', $file) or die "$!";
+
+ print "processing file: $file\n" unless $self->{debug} == 0;
+
+ my $teardown = 0;
+ my %properties = ();
+ my %teared = ();
+ my $in_teardown = 0;
+ my $seen_test = 0;
+
+ while (<$fh>) {
+ chomp $_;
+
+ # ignore comments and blank lines
+ next if $_ =~ /^[\s]{0,}[\/]{0,1}[\*]{1,2}/ or $_ eq '' or $_ =~ /^[\s]*\/\//;
+
+ # collect properties. strict mode uses all properties while in non strict mode non initialized & empty properties are used
+ my $pattern = $self->{strict} == 0
+ ? '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+(?=;|\s=\s(?:\[\]|null)))'
+ : '^[\s]*(?:private|public|protected)\s(?:static ){0,}([^\s]{0,})\s*\$([^\s;]+)';
+
+ if ($seen_test == 0 && $_ =~ /$pattern/) {
+ $properties{$2} = $1;
+ print " property: $2 type: $1\n" unless $self->{debug} == 0;
+
+ next;
+ }
+
+ # assuming class properties are not defined all over the place
+ if ($_ =~ 'public function test') {
+ $seen_test = 1;
+ }
+
+ # check teardown methods
+ if ($_ =~ '([\s]+)(?:protected |public )function tearDown\(\): void'
+ or $_ =~ '([\s]+)(?:protected |public )static function tearDownAfterClass\(\): void'
+ ) {
+ $teardown = 1;
+ $in_teardown = 1;
+ my $spaces = $1;
+
+ print " has teardown\n" unless $self->{debug} == 0;
+
+ while ($in_teardown == 1) {
+ my $line = <$fh>;
+ chomp $line;
+
+ my @matches = $line =~ /\$this->(\w+(?=(?:[ ,\)]|$)))/g;
+ my @statics = $line =~ /self::\$(\w+(?=(?:[ ,]|$)))/g;
+
+ foreach my $match (@matches, @statics) {
+ print " property: $match was found in teardown\n" unless $self->{debug} == 0;
+ $teared{$match} = 1;
+ }
+
+ if ($line =~ /$spaces}$/) {
+ $in_teardown = 0;
+ last;
+ }
+ }
+ }
+ }
+
+ close($fh);
+
+ $self->{teared}{$file} = GPH::PHPUnit::Teared->new((
+ file => $file,
+ teardown => $teardown,
+ properties => \%properties,
+ teared => \%teared,
+ ));
+ }
+
+ return ($self);
+};
+
+sub validate {
+ my ($self) = @_;
+ my $exit = 0;
+
+ foreach my $teared (sort keys %{$self->{teared}}) {
+ if ($self->{teared}{$teared}->isValid() != 1 && $exit == 0) {
+ $exit = 1;
+ }
+ }
+
+ return ($exit);
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+GPH::PHPUnit::Teardown - module to validate correct teardown behaviour of PHPUnit test classes.
+
+see https://docs.phpunit.de/en/10.5/fixtures.html#more-setup-than-teardown for further information
+
+=head1 SYNOPSIS
+
+ use GPH::PHPUnit::Teardown;
+
+ my $teardown = GPH::PHPUnit::Teardown->new((files => ['foo.php', 'bar.php'], debug => 1);
+ $teardown->parse();
+
+=head1 METHODS
+
+=over 4
+
+=item C<< -Enew(%args) >>
+
+the C method returns a GPH::PHPUnit::Teardown object. it takes a hash of options, valid option keys include:
+
+=over
+
+=item files B<(required)>
+
+an array of file paths of files which you'd like to analyse.
+
+=item debug
+
+boolean whether or not to debug the parsing process.
+
+=item strict
+
+boolean whether or not to parse in strict mode (i.e. use all class properties regardless of initialisation state).
+
+=back
+
+=item C<< -Eparse() >>
+
+parse the files defined in C<< $self->{files} >>
+
+=item C<< -Evalidate() >>
+
+validate the parsed files. returns exit code 0 when all files are valid, 1 if one or more files are invalid
+
+=back
+
+=head1 AUTHOR
+
+the GPH::PHPUnit::Teardown 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
diff --git a/lib/GPH/PHPUnit/Teared.pm b/lib/GPH/PHPUnit/Teared.pm
new file mode 100644
index 0000000..7bcaed2
--- /dev/null
+++ b/lib/GPH/PHPUnit/Teared.pm
@@ -0,0 +1,111 @@
+package GPH::PHPUnit::Teared;
+
+use strict;
+use warnings FATAL => 'all';
+
+sub new {
+ my ($proto, %args) = @_;
+
+ exists($args{file}) or die "file must be defined: $!";
+
+ my $self = bless {
+ file => $args{file},
+ teardown => $args{teardown} // 0,
+ properties => $args{properties} // {},
+ teared => $args{teared} // {},
+ }, $proto;
+
+ return ($self);
+}
+
+sub isValid {
+ my ($self) = @_;
+ my $properties = keys %{$self->{properties}};
+
+ if ($properties > 0 && $self->{teardown} == 0) {
+ print "file $self->{file} is invalid: has properties, but no teardown\n";
+
+ return(0);
+ }
+
+ my @missing = ();
+ foreach (keys %{$self->{properties}}) {
+ push (@missing, $_) unless exists($self->{teared}{$_});
+ }
+
+ @missing = sort @missing;
+
+ my $missing = @missing;
+
+ if ($missing > 0) {
+ print "file $self->{file} is invalid: propert" . ($missing == 1 ? "y '": "ies '") . join("', '", @missing) . "' " . ($missing == 1 ? "is ": "are ") . "not teared down\n";
+
+ return(0);
+ }
+
+ return(1);
+};
+
+1;
+
+__END__
+
+=head1 NAME
+
+GPH::PHPUnit::Teared - data object for GPH::PHPUnit::Teardown module
+
+=head1 SYNOPSIS
+
+ use GPH::PHPUnit::Teared;
+
+ my $teared = GPH::PHPUnit::Teared->new((file => 'foo.php', teardown => 1, properties => ('bar' => 1), teared => ('bar' => 1)));
+ $teared->isValid();
+
+=head1 METHODS
+
+=over 4
+
+=item C<< -Enew(%args) >>
+
+the C method returns a GPH::PHPUnit::Teared object. it takes a hash of options, valid option keys include:
+
+=over
+
+=item file B<(required)>
+
+file (path) name used for validation output.
+
+=item teardown
+
+boolean whether or not the file contains a teardown method (can be tearDown or tearDownAfterClass).
+
+=item properties
+
+hash of class properties of the file
+
+=item teared
+
+hash of class properties which are 'touched' within a teardown method.
+
+=back
+
+=item C<< -EisValid() >>
+
+validates the teardown behaviour of the file:
+
+ - if it has properties, a teardown method is required.
+ - if it has properties and one or more teardown methods, all properties need to be 'touched' within those methods.
+
+returns 1 if valid, 0 otherwise.
+
+=back
+
+=head1 AUTHOR
+
+the GPH::PHPUnit::Teared 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
diff --git a/t/share/PHPUnit/InvalidTeardownTestCase.php b/t/share/PHPUnit/InvalidTeardownTestCase.php
new file mode 100644
index 0000000..7f6b8ee
--- /dev/null
+++ b/t/share/PHPUnit/InvalidTeardownTestCase.php
@@ -0,0 +1,29 @@
+
+ */
+class InvalidTeardownTestCase extends TestCase
+{
+ private ?int $foo = null;
+ private static array $fixtures = [];
+ private $bar;
+
+ public function tearDown(): void
+ {
+ unset($this->foo);
+ }
+
+ public function testFooBar(): void
+ {
+ }
+
+ protected static function tearDownAfterClass(): void
+ {
+ self::$fixtures = [];
+ }
+}
diff --git a/t/share/PHPUnit/TeardownTest.php b/t/share/PHPUnit/TeardownTest.php
new file mode 100644
index 0000000..4d926d9
--- /dev/null
+++ b/t/share/PHPUnit/TeardownTest.php
@@ -0,0 +1,39 @@
+
+ */
+class TeardownTest extends TestCase
+{
+ private ?int $foo = null;
+ private array $history = ['Foo', 'Bar'];
+ private string $bar = 'qux';
+ // comment
+ private array $fixtures = [];
+ private Configuration $config;
+ public static ?FooProvider $fooProvider;
+ public static BarProvider $barProvider;
+
+ public function tearDown(): void
+ {
+ unset($this->foo, $this->fixtures);
+
+ $this->config->reset();
+ }
+
+ public function testFooBar(): void
+ {
+ }
+
+ protected static function tearDownAfterClass(): void
+ {
+ self::$barProvider->reset();
+ self::$fooProvider = null;
+ }
+
+ private Processor $processor;
+}
diff --git a/t/share/PHPUnit/ValidTeardownTestCase.php b/t/share/PHPUnit/ValidTeardownTestCase.php
new file mode 100644
index 0000000..23fd3a5
--- /dev/null
+++ b/t/share/PHPUnit/ValidTeardownTestCase.php
@@ -0,0 +1,28 @@
+
+ */
+class ValidTeardownTestCase extends TestCase
+{
+ private ?int $foo = null;
+ private static array $fixtures = [];
+
+ public function tearDown(): void
+ {
+ unset($this->foo);
+ }
+
+ public function testFooBar(): void
+ {
+ }
+
+ protected static function tearDownAfterClass(): void
+ {
+ self::$fixtures = [];
+ }
+}
diff --git a/t/unit/GPH/PHPUnit/Teardown.t b/t/unit/GPH/PHPUnit/Teardown.t
new file mode 100644
index 0000000..1f24213
--- /dev/null
+++ b/t/unit/GPH/PHPUnit/Teardown.t
@@ -0,0 +1,291 @@
+#!/usr/bin/perl
+package t::unit::GPH::PHPUnit::Teardown;
+
+use strict;
+use warnings;
+
+use Test2::V0 -target => 'GPH::PHPUnit::Teardown';
+use Test2::Tools::Spec;
+
+use Data::Dumper;
+
+describe "class `$CLASS` instantiation" => sub {
+ tests 'it can be instantiated' => sub {
+ can_ok($CLASS, 'new');
+ };
+
+ tests "mandatory config options" => sub {
+ ok(dies {$CLASS->new()}, 'died with missing files option') or note($@);
+ };
+
+ tests 'instantiation with default values' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((files => ['foo.php']));
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field files => array {
+ item 'foo.php';
+ end;
+ };
+ field debug => 0;
+ field strict => 0;
+ field teared => hash {
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+
+ tests 'instantiation with given values' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((files => ['foo.php'], debug => 1, strict => 1));
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field files => array {
+ item 'foo.php';
+ end;
+ };
+ field debug => 1;
+ field strict => 1;
+ field teared => hash {
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+};
+
+describe "class `$CLASS` parse method" => sub {
+ my $file = 't/share/PHPUnit/TeardownTest.php';
+
+ tests "parse non existing file" => sub {
+ ok(dies {$CLASS->new(files => ['fooTest.php'])->parse()}, 'died with non existing file') or note($@);
+ };
+
+ tests 'parse text file' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((files => ['t/share/PHPUnit/phpunit-baseline.txt']))->parse();
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field files => array {
+ item 't/share/PHPUnit/phpunit-baseline.txt';
+ end;
+ };
+ field debug => 0;
+ field strict => 0;
+ field teared => hash {
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+
+ tests 'parse test class' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((files => [$file]))->parse();
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field files => array {
+ item $file;
+ end;
+ };
+ field debug => 0;
+ field strict => 0;
+ field teared => hash {
+ field $file => object {
+ prop blessed => 'GPH::PHPUnit::Teared';
+
+ field file => $file;
+ field teardown => 1;
+ field properties => hash {
+ field 'foo' => '?int';
+ field 'fixtures' => 'array';
+ field 'config' => 'Configuration';
+ field 'fooProvider' => '?FooProvider';
+ field 'barProvider' => 'BarProvider';
+ end;
+ };
+ field teared => hash {
+ field 'foo' => 1;
+ field 'fixtures' => 1;
+ field 'fooProvider' => 1;
+ end;
+ };
+ };
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+
+ tests 'parse test class with debug' => sub {
+ my ($object, $exception, $warnings, $stdout);
+
+ my $expected = "processing file: t/share/PHPUnit/TeardownTest.php
+ property: foo type: ?int
+ property: fixtures type: array
+ property: config type: Configuration
+ property: fooProvider type: ?FooProvider
+ property: barProvider type: BarProvider
+ has teardown
+ property: foo was found in teardown
+ property: fixtures was found in teardown
+ has teardown
+ property: fooProvider was found in teardown";
+
+ $exception = dies {
+ $warnings = warns {
+ local *STDOUT;
+
+ open *STDOUT, '>', \$stdout;
+
+ $object = $CLASS->new((files => [$file], debug => 1))->parse();
+
+ close *STDOUT;
+ chomp $stdout;
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($stdout, $expected, 'stdout as expected') or diag Dumper($stdout);
+ };
+
+ tests 'parse test class in strict mode' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((files => [$file], strict => 1))->parse();
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field files => array {
+ item $file;
+ end;
+ };
+ field debug => 0;
+ field strict => 1;
+ field teared => hash {
+ field $file => object {
+ prop blessed => 'GPH::PHPUnit::Teared';
+
+ field file => $file;
+ field teardown => 1;
+ field properties => hash {
+ field 'fixtures' => 'array';
+ field 'history' => 'array';
+ field 'config' => 'Configuration';
+ field 'foo' => '?int';
+ field 'barProvider' => 'BarProvider';
+ field 'bar' => 'string';
+ field 'fooProvider' => '?FooProvider';
+ end;
+ };
+ field teared => hash {
+ field 'foo' => 1;
+ field 'fixtures' => 1;
+ field 'fooProvider' => 1;
+ end;
+ };
+ };
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+};
+describe "class `$CLASS` validate method" => sub {
+ tests 'validate' => sub {
+ my ($result, $object, $stdout, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ local *STDOUT;
+
+ open *STDOUT, '>', \$stdout;
+
+ $object = $CLASS
+ ->new((files => ['t/share/PHPUnit/ValidTeardownTestCase.php', 't/share/PHPUnit/TeardownTest.php', 't/share/PHPUnit/InvalidTeardownTestCase.php']))
+ ->parse();
+
+ $result = $object->validate();
+
+ close *STDOUT;
+ chomp $stdout;
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($result, 1, 'teardown check has invalid files') or diag Dumper($object);
+ is($stdout, 'file t/share/PHPUnit/InvalidTeardownTestCase.php is invalid: property \'bar\' is not teared down
+file t/share/PHPUnit/TeardownTest.php is invalid: properties \'barProvider\', \'config\' are not teared down', 'stdout as expected') or diag Dumper($stdout);
+ };
+};
+
+done_testing();
+
diff --git a/t/unit/GPH/PHPUnit/Teared.t b/t/unit/GPH/PHPUnit/Teared.t
new file mode 100644
index 0000000..badcb3f
--- /dev/null
+++ b/t/unit/GPH/PHPUnit/Teared.t
@@ -0,0 +1,201 @@
+#!/usr/bin/perl
+package t::unit::GPH::PHPUnit::Teared;
+
+use strict;
+use warnings;
+
+use Test2::V0 -target => 'GPH::PHPUnit::Teared';
+use Test2::Tools::Spec;
+
+use Data::Dumper;
+
+describe "class `$CLASS` instantiation" => sub {
+ tests 'it can be instantiated' => sub {
+ can_ok($CLASS, 'new');
+ };
+
+ tests "mandatory config options" => sub {
+ ok(dies {$CLASS->new()}, 'died with missing file option') or note($@);
+ };
+
+ tests 'instantiation with default values' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((file => 'foo.php'));
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field file => 'foo.php';
+ field teardown => 0;
+ field properties => hash {
+ end;
+ };
+ field teared => hash {
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+
+ tests 'instantiation with given values' => sub {
+ my ($object, $exception, $warnings);
+
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((
+ file => 'foo.php',
+ teardown => 1,
+ properties => { 'foo' => 1 },
+ teared => { 'foo' => 1 },
+ ));
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is(
+ $object,
+ object {
+ field file => 'foo.php';
+ field teardown => 1;
+ field properties => hash {
+ field 'foo' => 1;
+ end;
+ };
+ field teared => hash {
+ field 'foo' => 1;
+ end;
+ };
+ end;
+ },
+ 'object as expected',
+ Dumper($object)
+ );
+ };
+};
+
+describe "class `$CLASS` is valid method without teardown" => sub {
+ tests 'invalid no teardown' => sub {
+ my ($object, $exception, $warnings, $stdout);
+
+ $exception = dies {
+ $warnings = warns {
+ local *STDOUT;
+
+ open *STDOUT, '>', \$stdout;
+
+ $object = $CLASS->new((
+ file => 'foo.php',
+ teardown => 0,
+ properties => { 'foo' => 1 },
+ ))->isValid();
+
+ close *STDOUT;
+ chomp $stdout;
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($object, 0, 'file is not valid') or diag Dumper($object);
+ is($stdout, 'file foo.php is invalid: has properties, but no teardown', 'stdout correct') or diag Dumper($stdout);
+ };
+};
+
+describe "class `$CLASS` is valid method without teared down properties" => sub {
+ my ($object, $exception, $warnings, $stdout, %properties, $output);
+
+ case 'single property' => sub {
+ %properties = (
+ foo => 1,
+ );
+ $output = 'file foo.php is invalid: property \'foo\' is not teared down';
+ };
+
+ case 'multiple properties' => sub {
+ %properties = (
+ foo => 1,
+ bar => 1,
+ );
+ $output = 'file foo.php is invalid: properties \'bar\', \'foo\' are not teared down';
+ };
+
+ tests 'invalid properties not touched' => sub {
+ $exception = dies {
+ $warnings = warns {
+ local *STDOUT;
+
+ open *STDOUT, '>', \$stdout;
+
+ $object = $CLASS->new((
+ file => 'foo.php',
+ teardown => 1,
+ properties => \%properties,
+ ))->isValid();
+
+ close *STDOUT;
+ chomp $stdout;
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($object, 0, 'file is not valid') or diag Dumper($object);
+ is($stdout, $output, 'stdout correct') or diag Dumper($stdout);
+ };
+};
+
+describe "class `$CLASS` is valid" => sub {
+ my ($object, $exception, $warnings);
+
+ tests 'valid all properties are teared down' => sub {
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((
+ file => 'foo.php',
+ teardown => 1,
+ properties => { 'foo' => 1 },
+ teared => { 'foo' => 1 },
+ ))->isValid();
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($object, 1, 'file is valid') or diag Dumper($object);
+ };
+
+ tests 'valid no properties defined' => sub {
+ $exception = dies {
+ $warnings = warns {
+ $object = $CLASS->new((
+ file => 'foo.php',
+ teardown => 0,
+ ))->isValid();
+ };
+ };
+
+ is($exception, undef, 'no exception thrown');
+ is($warnings, 0, 'no warnings generated');
+
+ is($object, 1, 'file is valid') or diag Dumper($object);
+ };
+};
+
+done_testing();
+