Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

phpunit teardown validation #29

Merged
merged 1 commit into from
Jul 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/GPH.pm
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package GPH;
use strict;
use warnings FATAL => 'all';

our $VERSION = '1.4.0';
our $VERSION = '1.4.1';

1;

Expand Down Expand Up @@ -55,7 +55,7 @@ generate custom configuration file for L<Psalm|https://psalm.dev/>

=head1 VERSION

1.4.0
1.4.1

=head1 AUTHOR

Expand Down
178 changes: 178 additions & 0 deletions lib/GPH/PHPUnit/Teardown.pm
Original file line number Diff line number Diff line change
@@ -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<< -E<gt>new(%args) >>

the C<new> 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<< -E<gt>parse() >>

parse the files defined in C<< $self->{files} >>

=item C<< -E<gt>validate() >>

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 <wicliff.wolda@gmail.com>

=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
111 changes: 111 additions & 0 deletions lib/GPH/PHPUnit/Teared.pm
Original file line number Diff line number Diff line change
@@ -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<< -E<gt>new(%args) >>

the C<new> 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<< -E<gt>isValid() >>

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 <wicliff.wolda@gmail.com>

=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
29 changes: 29 additions & 0 deletions t/share/PHPUnit/InvalidTeardownTestCase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Foo\Bar;

/**
* @author wicliff <wicliff.wolda@gmail.com>
*/
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 = [];
}
}
39 changes: 39 additions & 0 deletions t/share/PHPUnit/TeardownTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

namespace Foo\Bar;

/**
* @author wicliff <wicliff.wolda@gmail.com>
*/
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;
}
Loading
Loading