diff --git a/lib/GPH/Gitlab.pm b/lib/GPH/Gitlab.pm index 2f7688b..37d65b7 100755 --- a/lib/GPH/Gitlab.pm +++ b/lib/GPH/Gitlab.pm @@ -9,6 +9,7 @@ # 2023-12-23 - added blacklist for more specific path definition # 2024-02-10 - namespaced module, bugfixes and unit tests # 2024-02-11 - constructor now requires named arguments +# 2024-02-18 - added support for default codeowners #------------------------------------------------------------------------------ package GPH::Gitlab; diff --git a/lib/GPH/PHPUnit.pm b/lib/GPH/PHPUnit.pm index ec19233..2a24891 100755 --- a/lib/GPH/PHPUnit.pm +++ b/lib/GPH/PHPUnit.pm @@ -53,7 +53,7 @@ sub new { bless $self, $class; if (exists($args{baseline}) and defined $args{baseline}) { - open(my $fh, '<', $args{baseline}) or die "unable to open phpunit baseline file $args{baseline} $!"; + open my $fh, '<', $args{baseline} or die $!; my @lines = (); while (<$fh>) { diff --git a/lib/GPH/PHPUnit/Stats.pm b/lib/GPH/PHPUnit/Stats.pm index 06a7089..f7ec7cd 100644 --- a/lib/GPH/PHPUnit/Stats.pm +++ b/lib/GPH/PHPUnit/Stats.pm @@ -5,6 +5,7 @@ # # Revisions: 2024-02-10 - created # 2024-02-11 - constructor now requires named arguments +# 2024-02-19 - fixed rounding bug #------------------------------------------------------------------------------ package GPH::PHPUnit::Stats; @@ -95,21 +96,22 @@ sub summary { # Returns: string sub footer { my $self = shift; + my $percentage = sprintf("%.2f", $self->{lines}->percentage()); - if ($self->{lines}->percentage() > $self->{threshold}) { + if ($percentage > $self->{threshold}) { return(sprintf( "\n ! [NOTE] Your coverage is %.2f%% percentage points over the required coverage.\n !%sConsider increasing the required coverage percentage.\n", ($self->{lines}->percentage() - $self->{threshold}), ' ' x 8 - )) + )); } - if ($self->{lines}->percentage() < $self->{threshold}) { + if ($percentage < $self->{threshold}) { return(sprintf( "\n ! [FAILED] Your coverage is %.2f%% percentage points under the required coverage.\n !%sPlease increase coverage by improving your tests.\n", ($self->{threshold} - $self->{lines}->percentage()), ' ' x 10 - )) + )); } }; diff --git a/lib/GPH/Psalm.pm b/lib/GPH/Psalm.pm index 267ee23..8fac36f 100644 --- a/lib/GPH/Psalm.pm +++ b/lib/GPH/Psalm.pm @@ -131,7 +131,7 @@ sub getConfigWithIssueHandlers { my ($handlers) = $dom->findnodes('//*[local-name()="issueHandlers"]'); foreach my $exclude ($blacklist) { - next if not defined $exclude; + next unless defined $exclude; my ($remove) = $handlers->findnodes("//*[local-name()=\"${exclude}\"]"); diff --git a/t/share/Gitlab/CODEOWNERS b/t/share/Gitlab/CODEOWNERS index ac41839..a1d0cbd 100644 --- a/t/share/Gitlab/CODEOWNERS +++ b/t/share/Gitlab/CODEOWNERS @@ -10,4 +10,7 @@ .gitlab-ci.yml ^[gamma][3] @teams/gamma -/tests/Unit/Service/ \ No newline at end of file +/tests/Unit/Service/ + +[delta] +/tests/Unit/Command \ No newline at end of file diff --git a/t/share/PHPStan/phpstan-includes.neon b/t/share/PHPStan/phpstan-includes.neon new file mode 100644 index 0000000..c9d09cd --- /dev/null +++ b/t/share/PHPStan/phpstan-includes.neon @@ -0,0 +1,11 @@ +includes: + - /includes/ + +parameters: + level: 4 + tmpDir: var + parallel: + maximumNumberOfProcesses: 4 + paths: + - src/Service/PhraseTagService.php + - src/Command/AbstractPhraseKeyCommand.php \ No newline at end of file diff --git a/t/unit/GPH/Composer.t b/t/unit/GPH/Composer.t index ce35ccb..4dfd6a4 100644 --- a/t/unit/GPH/Composer.t +++ b/t/unit/GPH/Composer.t @@ -9,8 +9,6 @@ use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - use constant CLASSMAP_FILE => './t/share/Composer/autoload_classmap.php'; describe "class `$CLASS`" => sub { @@ -35,12 +33,12 @@ describe "class `$CLASS`" => sub { }; tests "classmap not found" => sub { - ok(dies{$CLASS->new((classmap => 'foo.php'))}, 'died with classmap not found') or note ($@); + ok(dies {$CLASS->new((classmap => 'foo.php'))}, 'died with classmap not found') or note($@); }; tests "mandatory config options" => sub { - ok(dies{$CLASS->new(())}, 'died with missing classmap option') or note ($@); - ok(lives{$CLASS->new((classmap => CLASSMAP_FILE))}, 'lived with mandatory options') or note ($@); + ok(dies {$CLASS->new(())}, 'died with missing classmap option') or note($@); + ok(lives {$CLASS->new((classmap => CLASSMAP_FILE))}, 'lived with mandatory options') or note($@); }; }; @@ -69,7 +67,7 @@ describe 'test matching' => sub { }; tests 'match' => sub { - my ( $object, $exception, $warnings, $result ); + my ($object, $exception, $warnings, $result); $exception = dies { $warnings = warns { diff --git a/t/unit/GPH/Gitlab.t b/t/unit/GPH/Gitlab.t index 0477e47..af16843 100644 --- a/t/unit/GPH/Gitlab.t +++ b/t/unit/GPH/Gitlab.t @@ -11,8 +11,6 @@ use Data::Dumper; use constant CODEOWNERS_FILE => './t/share/Gitlab/CODEOWNERS'; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { my %config = ( codeowners => CODEOWNERS_FILE, @@ -68,7 +66,7 @@ describe "class `$CLASS`" => sub { item '/src/Service/'; end; }, - 'GetPaths call correct' + 'GetPaths call correct' ); is($object->getBlacklistPaths(), @@ -76,7 +74,7 @@ describe "class `$CLASS`" => sub { item '/src/Command/Config/ConfigPhraseKeyCommand.php'; end; }, - 'GetBlacklistPaths call correct' + 'GetBlacklistPaths call correct' ); is($object->getCommaSeparatedPathList(), '/src/Command/,/src/Service/', 'GetCommaSeparatedPathList call correct'); @@ -90,7 +88,7 @@ describe "class `$CLASS`" => sub { item '/src/Command/'; end; }, - 'Intersect call correct' + 'Intersect call correct' ); is($object->match('/src/Service/'), 1, 'Match call match correct'); @@ -102,21 +100,30 @@ describe "class `$CLASS`" => sub { }; describe 'test codeowners syntax' => sub { - my ($object, %config, $owner, $expected_paths, $exception, $warnings); + my ($object, %config, $owner, $expected_paths, $expected_blacklist, $exception, $warnings); case 'section with default owner' => sub { $owner = '@teams/beta'; $expected_paths = [ '/src/Command/Config/ConfigPhraseKeyCommand.php', '/src/DependencyInjection/' ]; + $expected_blacklist = []; }; case 'optional section with default owner and required approvals' => sub { $owner = '@teams/gamma'; $expected_paths = [ '/tests/Unit/Service/' ]; + $expected_blacklist = []; }; case 'owner with email' => sub { $owner = 'john@doe.com'; $expected_paths = [ '/src/Service/' ]; + $expected_blacklist = []; + }; + + case 'non defined owner' => sub { + $owner = 'jane@doe.com'; + $expected_paths = [ ]; + $expected_blacklist = []; }; tests 'module get paths' => sub { @@ -135,6 +142,7 @@ describe 'test codeowners syntax' => sub { is($exception, undef, 'no exception thrown'); is($warnings, 0, 'no warnings generated'); is($object->getPaths(), $expected_paths, 'paths correct') or diag Dumper($object); + is($object->getBlacklistPaths(), $expected_blacklist, 'blacklist correct') or diag Dumper($object); }; }; diff --git a/t/unit/GPH/Infection.t b/t/unit/GPH/Infection.t index 7cdaba9..5cb1e7f 100644 --- a/t/unit/GPH/Infection.t +++ b/t/unit/GPH/Infection.t @@ -7,17 +7,15 @@ use warnings; use Test2::V0 -target => 'GPH::Infection'; use Test2::Tools::Spec; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); }; tests "mandatory config options" => sub { - ok(dies{$CLASS->new((msi => '9.0'))}, 'died with missing covered') or note ($@); - ok(dies{$CLASS->new((covered => '9.0'))}, 'died with missing msi') or note ($@); - ok(lives{$CLASS->new((msi => '9.0', covered => '9.0'))}, 'lives with mandatory options') or note ($@); + ok(dies {$CLASS->new((msi => '9.0'))}, 'died with missing covered') or note($@); + ok(dies {$CLASS->new((covered => '9.0'))}, 'died with missing msi') or note($@); + ok(lives {$CLASS->new((msi => '9.0', covered => '9.0'))}, 'lives with mandatory options') or note($@); }; }; diff --git a/t/unit/GPH/PHPMD.t b/t/unit/GPH/PHPMD.t index 08e6acf..dda8760 100644 --- a/t/unit/GPH/PHPMD.t +++ b/t/unit/GPH/PHPMD.t @@ -7,17 +7,15 @@ use warnings; use Test2::V0 -target => 'GPH::PHPMD'; use Test2::Tools::Spec; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); }; tests "mandatory config options" => sub { - ok(dies{$CLASS->new((owner =>'@teams/alpha'))}, 'died with missing cyclo level option') or note ($@); - ok(dies{$CLASS->new((cyclo_level => 8))}, 'died with missing owner option') or note ($@); - ok(lives{$CLASS->new((owner =>'@teams/alpha', cyclo_level => 8))}, 'lived with mandatory options') or note ($@); + ok(dies {$CLASS->new((owner => '@teams/alpha'))}, 'died with missing cyclo level option') or note($@); + ok(dies {$CLASS->new((cyclo_level => 8))}, 'died with missing owner option') or note($@); + ok(lives {$CLASS->new((owner => '@teams/alpha', cyclo_level => 8))}, 'lived with mandatory options') or note($@); }; tests 'instantation' => sub { @@ -25,7 +23,7 @@ describe "class `$CLASS`" => sub { $exception = dies { $warnings = warns { - $object = $CLASS->new((owner =>'@teams/alpha', cyclo_level => 3)); + $object = $CLASS->new((owner => '@teams/alpha', cyclo_level => 3)); }; }; @@ -49,11 +47,11 @@ describe "class `$CLASS`" => sub { describe "class `$CLASS` config generation" => sub { tests 'compare config contents' => sub { - my $object = $CLASS->new((owner =>'@teams/alpha', cyclo_level => 3)); + my $object = $CLASS->new((owner => '@teams/alpha', cyclo_level => 3)); my $config = $object->getConfig(); my $mock; - open (my $fh, '<', './t/share/PHPMD/phpmd-ruleset.xml'); + open(my $fh, '<', './t/share/PHPMD/phpmd-ruleset.xml'); { local $/; $mock = <$fh>; diff --git a/t/unit/GPH/PHPStan.t b/t/unit/GPH/PHPStan.t index 3c117c2..dbb7fe7 100644 --- a/t/unit/GPH/PHPStan.t +++ b/t/unit/GPH/PHPStan.t @@ -8,17 +8,15 @@ use Test2::V0 -target => 'GPH::PHPStan'; use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); }; tests 'dies without correct config' => sub { - ok(dies{$CLASS->new(('level' => 4))}, 'died with paths missing') or note ($@); - ok(dies{$CLASS->new(('paths' => []))}, 'died with level missing') or note ($@); - ok(lives{$CLASS->new(('level' => 4, 'paths' => []))}, 'lives with mandatory config settings') or note ($@); + ok(dies {$CLASS->new(('level' => 4))}, 'died with paths missing') or note($@); + ok(dies {$CLASS->new(('paths' => []))}, 'died with level missing') or note($@); + ok(lives {$CLASS->new(('level' => 4, 'paths' => []))}, 'lives with mandatory config settings') or note($@); }; }; @@ -43,6 +41,22 @@ describe "class `$CLASS` instantiation values" => sub { $config_path = './t/share/PHPStan/phpstan-min.neon'; }; + case 'includes, no baseline' => sub { + %config = ( + level => 4, + paths => \@paths, + includes => [ '/includes/' ], + ); + + $expected_level = 4; + $expected_baseline = undef; + $expected_ignoredDirectories = undef; + $expected_cacheDir = 'var'; + $expected_includes = [ '/includes/' ]; + $expected_threads = 4; + $config_path = './t/share/PHPStan/phpstan-includes.neon'; + }; + case 'config with empty array for ignores' => sub { %config = ( level => 4, @@ -126,7 +140,7 @@ describe "class `$CLASS` instantiation values" => sub { is($exception, undef, 'no exception thrown', Dumper($object)); is($warnings, 0, 'no warnings generated', Dumper($object)); - open (my $fh, '<', $config_path); + open(my $fh, '<', $config_path); { local $/; $mock = <$fh>; diff --git a/t/unit/GPH/PHPUnit.t b/t/unit/GPH/PHPUnit.t index 96c23fb..3ee0668 100644 --- a/t/unit/GPH/PHPUnit.t +++ b/t/unit/GPH/PHPUnit.t @@ -14,8 +14,6 @@ use constant CLASSMAP_FILE => './t/share/Composer/autoload_classmap.php'; use constant PHPUNIT_OUTPUT_FILE => './t/share/PHPUnit/phpunit-output.txt'; use constant PHPUNIT_BASELINE_FILE => './t/share/PHPUnit/phpunit-baseline.txt'; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); @@ -29,8 +27,8 @@ describe "class `$CLASS`" => sub { }; tests "baseline file not found" => sub { - ok(dies{$CLASS->new((codeowners => CODEOWNERS_FILE, owner =>'@teams/alpha', baseline => 'foo.txt'))}, 'died with baseline not found') or note ($@); - ok(lives{$CLASS->new((codeowners => CODEOWNERS_FILE, owner =>'@teams/alpha', classmap => CLASSMAP_FILE, baseline => PHPUNIT_BASELINE_FILE))}, 'lives with correct baseline') or note ($@); + ok(dies {$CLASS->new((codeowners => CODEOWNERS_FILE, owner => '@teams/alpha', baseline => 'foo.txt'))}, 'died with baseline not found') or note($@); + ok(lives {$CLASS->new((codeowners => CODEOWNERS_FILE, owner => '@teams/alpha', classmap => CLASSMAP_FILE, baseline => PHPUNIT_BASELINE_FILE))}, 'lives with correct baseline') or note($@); }; }; @@ -43,6 +41,7 @@ describe 'configuration options' => sub { owner => $owner, classmap => CLASSMAP_FILE, codeowners => CODEOWNERS_FILE, + baseline => undef, ); $expected_threshold = 0.0; @@ -55,7 +54,7 @@ describe 'configuration options' => sub { classmap => CLASSMAP_FILE, codeowners => CODEOWNERS_FILE, threshold => 95.5, - excludes => ['.gitlab-ci.yml'], + excludes => [ '.gitlab-ci.yml' ], baseline => PHPUNIT_BASELINE_FILE ); @@ -106,7 +105,7 @@ describe "parsing phpunit report output" => sub { owner => '@teams/alpha', classmap => CLASSMAP_FILE, codeowners => CODEOWNERS_FILE, - excludes => ['.gitlab-ci.yml'], + excludes => [ '.gitlab-ci.yml' ], baseline => PHPUNIT_BASELINE_FILE ); diff --git a/t/unit/GPH/PHPUnit/Stat.t b/t/unit/GPH/PHPUnit/Stat.t index 425ee8a..bb9f370 100644 --- a/t/unit/GPH/PHPUnit/Stat.t +++ b/t/unit/GPH/PHPUnit/Stat.t @@ -9,8 +9,6 @@ use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); diff --git a/t/unit/GPH/PHPUnit/Stats.t b/t/unit/GPH/PHPUnit/Stats.t index 06062c8..5b25c00 100644 --- a/t/unit/GPH/PHPUnit/Stats.t +++ b/t/unit/GPH/PHPUnit/Stats.t @@ -9,8 +9,6 @@ use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - my %config = ( owner => '@teams/alpha', threshold => 95.01 @@ -22,8 +20,8 @@ describe "class `$CLASS`" => sub { }; tests "mandatory config options" => sub { - ok(dies{$CLASS->new((threshold => '1'))}, 'died with missing owner option') or note ($@); - ok(lives{$CLASS->new((owner => '@teams/alpha'))}, 'lives with mandatory options') or note ($@); + ok(dies {$CLASS->new((threshold => '1'))}, 'died with missing owner option') or note($@); + ok(lives {$CLASS->new((owner => '@teams/alpha'))}, 'lives with mandatory options') or note($@); }; }; @@ -72,6 +70,7 @@ describe 'class methods' => sub { 'Methods: 80.00% ( 4/ 5) Lines: 97.96% ( 48/ 49)', 'Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 18/ 18)', 'Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 1/ 1)', + 'Methods: 0.00% ( 0/ 0) Lines: 0.00% ( 0/ 0)', ); tests 'test add method' => sub { @@ -156,15 +155,20 @@ describe "class method" => sub { my ($object, $exception, $warnings, $threshold, $expected_footer); case "coverage higher than threshold" => sub { - $threshold = 95.0; + $threshold = '95.0'; $expected_footer = qr{! \[NOTE\] Your coverage is [0-9]+\.[0-9]{2}% percentage points over the required coverage}; }; case "coverage lower than threshold" => sub { - $threshold = 100.0; + $threshold = '100.0'; $expected_footer = qr{! \[FAILED\] Your coverage is [0-9]+\.[0-9]{2}% percentage points under the required coverage}; }; + case "coverage equal to threshold" => sub { + $threshold = '97.96'; + $expected_footer = ''; + }; + tests "test footer method" => sub { $exception = dies { $warnings = warns { @@ -177,7 +181,7 @@ describe "class method" => sub { is($exception, undef, 'no exception thrown'); is($warnings, 0, 'no warnings generated'); - like($object->footer(), $expected_footer, 'footer as expected'); + like($object->footer(), $expected_footer, 'footer as expected', Dumper($object)); }; }; diff --git a/t/unit/GPH/Psalm.t b/t/unit/GPH/Psalm.t index 944b231..a998e47 100644 --- a/t/unit/GPH/Psalm.t +++ b/t/unit/GPH/Psalm.t @@ -8,17 +8,15 @@ use Test2::V0 -target => 'GPH::Psalm'; use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); }; tests "mandatory config options" => sub { - ok(dies{$CLASS->new((level => '1'))}, 'died with missing paths') or note ($@); - ok(dies{$CLASS->new((paths => []))}, 'died with missing level') or note ($@); - ok(lives{$CLASS->new((level => '1', paths => []))}, 'lives with mandatory options') or note ($@); + ok(dies {$CLASS->new((level => '1'))}, 'died with missing paths') or note($@); + ok(dies {$CLASS->new((paths => []))}, 'died with missing level') or note($@); + ok(lives {$CLASS->new((level => '1', paths => []))}, 'lives with mandatory options') or note($@); }; }; @@ -27,9 +25,9 @@ describe 'configuration options' => sub { my ($expected_baseline, $expected_baselineCheck, $expected_ignoredDirectories, $expected_cacheDir, $expected_plugins); $level = 2; - @paths = ['src/Service/PhraseTagService.php', 'src/Command/AbstractPhraseKeyCommand.php']; - @ignoredDirectories = ['/ignored/']; - @plugins = ['Psalm\SymfonyPsalmPlugin\Plugin']; + @paths = [ 'src/Service/PhraseTagService.php', 'src/Command/AbstractPhraseKeyCommand.php' ]; + @ignoredDirectories = [ '/ignored/' ]; + @plugins = [ 'Psalm\SymfonyPsalmPlugin\Plugin' ]; case 'minimal options' => sub { %config = ( @@ -115,11 +113,11 @@ describe "class `$CLASS` config generation" => sub { my %config = ( level => 2, paths => \@paths, - ignoredDirectories => ['vendor'], + ignoredDirectories => [ 'vendor' ], baseline => 'baselines/psalm-baseline.xml', baselineCheck => 'true', cacheDir => './psalm', - plugins => ['Psalm\SymfonyPsalmPlugin\Plugin'], + plugins => [ 'Psalm\SymfonyPsalmPlugin\Plugin' ], ); my $object = $CLASS->new(%config); @@ -128,7 +126,7 @@ describe "class `$CLASS` config generation" => sub { my $config = $object->getConfig(); my $mock; - open (my $fh, '<', './t/share/Psalm/psalm.xml'); + open(my $fh, '<', './t/share/Psalm/psalm.xml'); local $/; $mock = <$fh>; @@ -142,7 +140,7 @@ describe "class `$CLASS` config generation" => sub { my $config = $object->getConfigWithIssueHandlers('./t/share/Psalm/psalm-stub.xml', qw{MoreSpecificImplementedParamType NonExistingHandler}); my $mock; - open (my $fh, '<', './t/share/Psalm/psalm-issue-handlers.xml'); + open(my $fh, '<', './t/share/Psalm/psalm-issue-handlers.xml'); { local $/; $mock = <$fh>; diff --git a/t/unit/GPH/XMLHelper.t b/t/unit/GPH/XMLHelper.t index f93d5e8..0c841d1 100644 --- a/t/unit/GPH/XMLHelper.t +++ b/t/unit/GPH/XMLHelper.t @@ -8,8 +8,6 @@ use Test2::V0 -target => 'GPH::XMLHelper'; use Test2::Tools::Spec; use Data::Dumper; -local $SIG{__WARN__} = sub {}; - describe "class `$CLASS`" => sub { tests 'it can be instantiated' => sub { can_ok($CLASS, 'new'); @@ -97,41 +95,43 @@ describe 'test element' => sub { }; }; -describe 'test paren element' => sub { +describe 'test parent element' => sub { my ($object, $dom, $element, $exception, $warnings); - $exception = dies { - $warnings = warns { - $object = $CLASS->new(); - $element = $object->buildElement(( - name => 'foo', - attributes => { - 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' - } - )); - - $object->buildElement(( - name => 'bar', - parent => $element, - value => 'baz', - )); - - $dom = $object->getDom(); - $dom->setDocumentElement($element); + tests 'element generation' => sub { + $exception = dies { + $warnings = warns { + $object = $CLASS->new(); + $element = $object->buildElement(( + name => 'foo', + attributes => { + 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance' + } + )); + + $object->buildElement(( + name => 'bar', + parent => $element, + value => 'baz', + )); + + $dom = $object->getDom(); + $dom->setDocumentElement($element); + }; }; - }; - is($exception, undef, 'no exception thrown'); - is($warnings, 0, 'no warnings generated'); + is($exception, undef, 'no exception thrown'); + is($warnings, 0, 'no warnings generated'); - is( - $dom->toString(), - ' + is( + $dom->toString(), + ' baz ', - 'parent child element as expected', - Dumper($dom->toString()) - ) + 'parent child element as expected', + Dumper($dom->toString()) + ) + } }; done_testing();