Skip to content
Open
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
40 changes: 40 additions & 0 deletions .github/workflows/phpunit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,43 @@ jobs:
- uses: ramsey/composer-install@a8d0d959dab41457692a5e2041bd9b757a119e3f # 3.2.1

- run: composer test

coverage:
name: Coverage
runs-on: ubuntu-latest
permissions:
contents: read
actions: write
services:
mariadb:
image: mariadb:11
env:
MARIADB_ROOT_PASSWORD: root
MARIADB_DATABASE: wordpress_test
ports:
- 3306:3306
options: >-
--health-cmd="healthcheck.sh --connect --innodb_initialized"
--health-interval=5s
--health-timeout=5s
--health-retries=10
env:
WP_TESTS_DB_HOST: 127.0.0.1
WP_TESTS_DB_NAME: wordpress_test
WP_TESTS_DB_USER: root
WP_TESTS_DB_PASSWORD: root
steps:
- uses: actions/checkout@v5

- uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: '8.3'
extensions: mysqli
coverage: xdebug
tools: composer

- uses: ramsey/composer-install@a8d0d959dab41457692a5e2041bd9b757a119e3f # 3.2.1

- run: composer coverage

- uses: k1LoW/octocov-action@a167dc0dee441b7ffc45e1b862ab55ec0d87f278 # v1.5.2
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor/
/vendor-bin/*/vendor/
/tests/.coverage/
/.phpunit.result.cache
13 changes: 13 additions & 0 deletions .octocov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
coverage:
paths:
- tests/.coverage/clover.xml
acceptable: current >= 95% && current >= prev

diff:
datastores:
- artifact://${GITHUB_REPOSITORY}

report:
if: is_default_branch
datastores:
- artifact://${GITHUB_REPOSITORY}
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,19 @@ composer cs # PHPCS
composer stan # PHPStan
composer test # PHPUnit
composer ci # all of the above, in this order
composer coverage # PHPUnit with a coverage report for octocov
```

### Layout and coverage

Every file in `src/`, and the main plugin file, needs a test named after it.
`tests/Unit/StructureTest.php` enforces this and also fails on a test whose
source file no longer exists.

`composer coverage` writes `tests/.coverage/clover.xml`. In CI,
[octocov](https://github.com/k1LoW/octocov) fails the run when line coverage
is below 95% or below the last report of `main` (`.octocov.yml`).

### Tests

`composer install` brings in WordPress and the WordPress test suite, so the tests
Expand Down
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@
],
"test:unit": "phpunit",
"test:integration": "phpunit -c phpunit-integration.xml.dist",
"coverage": [
"@putenv XDEBUG_MODE=coverage",
"phpunit -c phpunit-coverage.xml.dist --coverage-clover=tests/.coverage/clover.xml"
],
"ci": [
"@lint",
"@cs",
Expand Down
25 changes: 25 additions & 0 deletions phpunit-coverage.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?xml version="1.0"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="tests/bootstrap-integration.php"
colors="true"
failOnWarning="true"
failOnRisky="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTestsThatDoNotTestAnything="true"
convertDeprecationsToExceptions="false">
<testsuites>
<testsuite name="unit">
<directory suffix="Test.php">tests/Unit</directory>
</testsuite>
<testsuite name="integration">
<directory suffix="Test.php">tests/Integration</directory>
</testsuite>
</testsuites>
<coverage>
<include>
<file>wp-simple-smtp.php</file>
<directory suffix=".php">src</directory>
</include>
</coverage>
</phpunit>
35 changes: 35 additions & 0 deletions tests/Support/PluginFiles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace LibreCodeCoop\SimpleSmtp\Tests\Support;

use FilesystemIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

final class PluginFiles {

public static function root() {
return dirname( __DIR__, 2 );
}

public static function under( $directory, $suffix ) {
$root = self::root() . '/' . $directory;

if ( ! is_dir( $root ) ) {
return array();
}

$paths = array();
$files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $root, FilesystemIterator::SKIP_DOTS ) );

foreach ( $files as $file ) {
if ( $file->isFile() && str_ends_with( $file->getPathname(), $suffix ) ) {
$paths[] = substr( $file->getPathname(), strlen( self::root() ) + 1 );
}
}

sort( $paths );

return $paths;
}
}
99 changes: 99 additions & 0 deletions tests/Unit/StructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

namespace LibreCodeCoop\SimpleSmtp\Tests\Unit;

use LibreCodeCoop\SimpleSmtp\Tests\Support\PluginFiles;
use PHPUnit\Framework\TestCase;

final class StructureTest extends TestCase {

private const PLUGIN_FILE = 'wp-simple-smtp.php';

/**
* @dataProvider provide_plugin_files
*/
public function test_a_file_of_the_plugin_is_covered_by_the_test_named_after_it( $file ) {
$this->assert_one_exists( self::tests_covering( $file ), $file . ' is not covered by' );
}

public static function provide_plugin_files() {
$files = array_merge(
array( self::PLUGIN_FILE ),
PluginFiles::under( 'src', '.php' )
);

foreach ( $files as $file ) {
yield $file => array( $file );
}
}

/**
* @dataProvider provide_test_files
*/
public function test_a_test_covers_a_file_of_the_plugin( $file ) {
$this->assert_one_exists( self::files_covered_by( $file ), $file . ' does not cover' );
}

public static function provide_test_files() {
$files = array_merge(
PluginFiles::under( 'tests/Unit', 'Test.php' ),
PluginFiles::under( 'tests/Integration', 'Test.php' ),
PluginFiles::under( 'tests/E2E', '.spec.ts' )
);

foreach ( $files as $file ) {
if ( 'tests/Unit/StructureTest.php' !== $file ) {
yield $file => array( $file );
}
}
}

private static function tests_covering( $file ) {
if ( self::PLUGIN_FILE === $file ) {
return array( 'tests/Integration/' . self::studly( basename( $file, '.php' ) ) . 'Test.php' );
}

$name = substr( $file, strlen( 'src/' ), -strlen( '.php' ) );

return array(
'tests/Unit/' . $name . 'Test.php',
'tests/Integration/' . $name . 'Test.php',
);
}

private static function files_covered_by( $file ) {
if ( str_starts_with( $file, 'tests/E2E/' ) ) {
return array( self::kebab( substr( $file, strlen( 'tests/E2E/' ), -strlen( '.spec.ts' ) ) ) . '.php' );
}

if ( str_starts_with( $file, 'tests/Unit/' ) ) {
return array( 'src/' . substr( $file, strlen( 'tests/Unit/' ), -strlen( 'Test.php' ) ) . '.php' );
}

$name = substr( $file, strlen( 'tests/Integration/' ), -strlen( 'Test.php' ) );

return array(
'src/' . $name . '.php',
self::kebab( $name ) . '.php',
);
}

private function assert_one_exists( array $files, $subject ) {
$found = array_filter(
$files,
static function ( $file ) {
return file_exists( PluginFiles::root() . '/' . $file );
}
);

$this->assertNotEmpty( $found, sprintf( '%s %s.', $subject, implode( ' or ', $files ) ) );
}

private static function studly( $name ) {
return str_replace( ' ', '', ucwords( str_replace( '-', ' ', $name ) ) );
}

private static function kebab( $name ) {
return strtolower( (string) preg_replace( '/(?<!^)[A-Z]/', '-$0', $name ) );
}
}
Loading