Skip to content

Commit e2c4d1f

Browse files
authored
Merge pull request #700 from ProgressPlanner/ari/add-phpunit-tests-20251031
Add PHPUnit Coverage workflow
2 parents 562d54b + 5540de8 commit e2c4d1f

21 files changed

Lines changed: 1211 additions & 286 deletions

.github/workflows/code-coverage.yml

Lines changed: 456 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Coverage Status Check - DISABLED
2+
3+
on:
4+
workflow_dispatch:
5+
6+
jobs:
7+
coverage-gate:
8+
name: Coverage Gate
9+
runs-on: ubuntu-latest
10+
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Wait for coverage check
16+
timeout-minutes: 10
17+
run: |
18+
echo "Waiting for Code Coverage Check to complete..."
19+
20+
# Wait up to 10 minutes for the check to appear and complete
21+
for i in {1..60}; do
22+
# Get the status of the Code Coverage Check
23+
STATUS=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs \
24+
--jq '.check_runs[] | select(.name == "Code Coverage Check") | .status' || echo "")
25+
26+
CONCLUSION=$(gh api repos/${{ github.repository }}/commits/${{ github.event.pull_request.head.sha }}/check-runs \
27+
--jq '.check_runs[] | select(.name == "Code Coverage Check") | .conclusion' || echo "")
28+
29+
if [ -n "$STATUS" ]; then
30+
echo "Check found with status: $STATUS, conclusion: $CONCLUSION"
31+
32+
if [ "$STATUS" == "completed" ]; then
33+
if [ "$CONCLUSION" == "success" ]; then
34+
echo "✅ Code coverage check passed!"
35+
exit 0
36+
else
37+
echo "❌ Code coverage check failed with conclusion: $CONCLUSION"
38+
exit 1
39+
fi
40+
fi
41+
else
42+
echo "Check not found yet (attempt $i/60)"
43+
fi
44+
45+
sleep 10
46+
done
47+
48+
echo "❌ Timeout waiting for Code Coverage Check"
49+
exit 1
50+
env:
51+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
52+
53+
- name: Coverage gate passed
54+
run: echo "✅ Code coverage requirements met!"

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[![Test](https://github.com/ProgressPlanner/progress-planner/actions/workflows/phpunit.yml/badge.svg)](https://github.com/ProgressPlanner/progress-planner/actions/workflows/phpunit.yml)
2+
[![Code Coverage](https://github.com/ProgressPlanner/progress-planner/actions/workflows/code-coverage.yml/badge.svg)](https://github.com/ProgressPlanner/progress-planner/actions/workflows/code-coverage.yml)
23
[![CS](https://github.com/ProgressPlanner/progress-planner/actions/workflows/cs.yml/badge.svg)](https://github.com/ProgressPlanner/progress-planner/actions/workflows/cs.yml)
34
[![PHPStan](https://github.com/ProgressPlanner/progress-planner/actions/workflows/phpstan.yml/badge.svg)](https://github.com/ProgressPlanner/progress-planner/actions/workflows/phpstan.yml)
45
[![Lint](https://github.com/ProgressPlanner/progress-planner/actions/workflows/lint.yml/badge.svg)](https://github.com/ProgressPlanner/progress-planner/actions/workflows/lint.yml)
@@ -24,6 +25,39 @@ This post explains what Progress Planner does and how to use it: [What does Prog
2425

2526
You can find [installation instructions here](https://prpl.fyi/install).
2627

28+
## Contributing
29+
30+
### Running Tests
31+
32+
To run the test suite:
33+
34+
```bash
35+
composer test
36+
```
37+
38+
### Code Coverage
39+
40+
To generate code coverage reports locally, you need either [PCOV](https://pecl.php.net/package/PCOV) (recommended) or [Xdebug](https://xdebug.org/) installed:
41+
42+
```bash
43+
composer coverage
44+
```
45+
46+
This will generate:
47+
- An HTML coverage report in the `coverage-html/` directory
48+
- A text-based coverage summary in your terminal
49+
50+
**Coverage Requirements:** Pull requests must maintain code coverage within 0.5% of the base branch. PRs that drop coverage by more than 0.5% will be blocked until additional tests are added.
51+
52+
### Other Quality Commands
53+
54+
```bash
55+
composer check-cs # Check coding standards
56+
composer fix-cs # Auto-fix coding standards
57+
composer phpstan # Run static analysis
58+
composer lint # Check PHP syntax
59+
```
60+
2761
## Branches on this repository
2862

2963
We use a couple of branches in this repository to keep things clean:

bin/install-wp-tests.sh

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
#!/usr/bin/env bash
2+
3+
if [ $# -lt 3 ]; then
4+
echo "usage: $0 <db-name> <db-user> <db-pass> [db-host] [wp-version] [skip-database-creation]"
5+
exit 1
6+
fi
7+
8+
DB_NAME=$1
9+
DB_USER=$2
10+
DB_PASS=$3
11+
DB_HOST=${4-localhost}
12+
WP_VERSION=${5-latest}
13+
SKIP_DB_CREATE=${6-false}
14+
15+
TMPDIR=${TMPDIR-/tmp}
16+
TMPDIR=$(echo $TMPDIR | sed -e "s/\/$//")
17+
WP_TESTS_DIR=${WP_TESTS_DIR-$TMPDIR/wordpress-tests-lib}
18+
WP_CORE_DIR=${WP_CORE_DIR-$TMPDIR/wordpress/}
19+
20+
download() {
21+
if [ `which curl` ]; then
22+
curl -s "$1" > "$2";
23+
elif [ `which wget` ]; then
24+
wget -nv -O "$2" "$1"
25+
fi
26+
}
27+
28+
if [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+\-(beta|RC)[0-9]+$ ]]; then
29+
WP_BRANCH=${WP_VERSION%\-*}
30+
WP_TESTS_TAG="branches/$WP_BRANCH"
31+
32+
elif [[ $WP_VERSION =~ ^[0-9]+\.[0-9]+$ ]]; then
33+
WP_TESTS_TAG="branches/$WP_VERSION"
34+
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
35+
if [[ $WP_VERSION =~ [0-9]+\.[0-9]+\.[0] ]]; then
36+
# version x.x.0 means the first release of the major version, so strip off the .0 and download version x.x
37+
WP_TESTS_TAG="tags/${WP_VERSION%??}"
38+
else
39+
WP_TESTS_TAG="tags/$WP_VERSION"
40+
fi
41+
elif [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
42+
WP_TESTS_TAG="trunk"
43+
else
44+
# http serves a single offer, whereas https serves multiple. we only want one
45+
download http://api.wordpress.org/core/version-check/1.7/ /tmp/wp-latest.json
46+
grep '[0-9]+\.[0-9]+(\.[0-9]+)?' /tmp/wp-latest.json
47+
LATEST_VERSION=$(grep -o '"version":"[^"]*' /tmp/wp-latest.json | sed 's/"version":"//')
48+
if [[ -z "$LATEST_VERSION" ]]; then
49+
echo "Latest WordPress version could not be found"
50+
exit 1
51+
fi
52+
WP_TESTS_TAG="tags/$LATEST_VERSION"
53+
fi
54+
set -ex
55+
56+
install_wp() {
57+
58+
if [ -d $WP_CORE_DIR ]; then
59+
return;
60+
fi
61+
62+
mkdir -p $WP_CORE_DIR
63+
64+
if [[ $WP_VERSION == 'nightly' || $WP_VERSION == 'trunk' ]]; then
65+
mkdir -p $TMPDIR/wordpress-trunk
66+
rm -rf $TMPDIR/wordpress-trunk/*
67+
svn export --quiet https://core.svn.wordpress.org/trunk $TMPDIR/wordpress-trunk/wordpress
68+
mv $TMPDIR/wordpress-trunk/wordpress/* $WP_CORE_DIR
69+
else
70+
if [ $WP_VERSION == 'latest' ]; then
71+
local ARCHIVE_NAME='latest'
72+
elif [[ $WP_VERSION =~ [0-9]+\.[0-9]+ ]]; then
73+
# https serves multiple offers, whereas http serves single.
74+
download https://wordpress.org/wordpress-$WP_VERSION.tar.gz $TMPDIR/wordpress.tar.gz
75+
ARCHIVE_NAME="wordpress-$WP_VERSION"
76+
fi
77+
78+
if [ ! -f $TMPDIR/wordpress.tar.gz ]; then
79+
download https://wordpress.org/latest.tar.gz $TMPDIR/wordpress.tar.gz
80+
fi
81+
tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WP_CORE_DIR
82+
fi
83+
84+
download https://raw.github.com/markoheijnen/wp-mysqli/master/db.php $WP_CORE_DIR/wp-content/db.php
85+
}
86+
87+
install_test_suite() {
88+
# portable in-place argument for both GNU sed and Mac OSX sed
89+
if [[ $(uname -s) == 'Darwin' ]]; then
90+
local ioption='-i.bak'
91+
else
92+
local ioption='-i'
93+
fi
94+
95+
# set up testing suite if it doesn't yet exist
96+
if [ ! -d $WP_TESTS_DIR ]; then
97+
# set up testing suite
98+
mkdir -p $WP_TESTS_DIR
99+
rm -rf $WP_TESTS_DIR/{includes,data}
100+
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/includes/ $WP_TESTS_DIR/includes
101+
svn export --quiet --ignore-externals https://develop.svn.wordpress.org/${WP_TESTS_TAG}/tests/phpunit/data/ $WP_TESTS_DIR/data
102+
fi
103+
104+
if [ ! -f wp-tests-config.php ]; then
105+
download https://develop.svn.wordpress.org/${WP_TESTS_TAG}/wp-tests-config-sample.php "$WP_TESTS_DIR"/wp-tests-config.php
106+
# remove all forward slashes in the end
107+
WP_CORE_DIR=$(echo $WP_CORE_DIR | sed "s:/\+$::")
108+
sed $ioption "s:dirname( __FILE__ ) . '/src/':'$WP_CORE_DIR/':" "$WP_TESTS_DIR"/wp-tests-config.php
109+
sed $ioption "s/youremptytestdbnamehere/$DB_NAME/" "$WP_TESTS_DIR"/wp-tests-config.php
110+
sed $ioption "s/yourusernamehere/$DB_USER/" "$WP_TESTS_DIR"/wp-tests-config.php
111+
sed $ioption "s/yourpasswordhere/$DB_PASS/" "$WP_TESTS_DIR"/wp-tests-config.php
112+
sed $ioption "s|localhost|${DB_HOST}|" "$WP_TESTS_DIR"/wp-tests-config.php
113+
fi
114+
115+
}
116+
117+
recreate_db() {
118+
shopt -s nocasematch
119+
if [[ $1 =~ ^(y|yes)$ ]]
120+
then
121+
mysqladmin drop $DB_NAME -f --user="$DB_USER" --password="$DB_PASS"$EXTRA
122+
create_db
123+
echo "Recreated the database ($DB_NAME)."
124+
else
125+
echo "Leaving the existing database ($DB_NAME) in place."
126+
fi
127+
shopt -u nocasematch
128+
}
129+
130+
create_db() {
131+
mysqladmin create $DB_NAME --user="$DB_USER" --password="$DB_PASS"$EXTRA
132+
}
133+
134+
install_db() {
135+
136+
if [ ${SKIP_DB_CREATE} = "true" ]; then
137+
return 0
138+
fi
139+
140+
# parse DB_HOST for port or socket references
141+
local PARTS=(${DB_HOST//\:/ })
142+
local DB_HOSTNAME=${PARTS[0]};
143+
local DB_SOCK_OR_PORT=${PARTS[1]};
144+
local EXTRA=""
145+
146+
if ! [ -z $DB_HOSTNAME ] ; then
147+
if [ $(echo $DB_SOCK_OR_PORT | grep -e '^[0-9]\{1,\}$') ]; then
148+
EXTRA=" --host=$DB_HOSTNAME --port=$DB_SOCK_OR_PORT --protocol=tcp"
149+
elif ! [ -z $DB_SOCK_OR_PORT ] ; then
150+
EXTRA=" --socket=$DB_SOCK_OR_PORT"
151+
elif ! [ -z $DB_HOSTNAME ] ; then
152+
EXTRA=" --host=$DB_HOSTNAME --protocol=tcp"
153+
fi
154+
fi
155+
156+
# create database
157+
if [ $(mysql --user="$DB_USER" --password="$DB_PASS"$EXTRA --execute='show databases;' | grep ^$DB_NAME$) ]
158+
then
159+
echo "Reinstalling will delete the existing test database ($DB_NAME)"
160+
read -p 'Are you sure you want to proceed? [y/N]: ' DELETE_EXISTING_DB
161+
recreate_db $DELETE_EXISTING_DB
162+
else
163+
create_db
164+
fi
165+
}
166+
167+
install_wp
168+
install_test_suite
169+
install_db

classes/admin/class-page-settings.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,27 +58,28 @@ public function add_admin_page_content() {
5858
public function get_settings() {
5959
$settings = [];
6060
foreach ( \progress_planner()->get_page_types()->get_page_types() as $page_type ) {
61-
if ( ! $this->should_show_setting( $page_type['slug'] ) ) {
61+
$slug = (string) $page_type['slug']; // @phpstan-ignore offsetAccess.invalidOffset
62+
if ( ! $this->should_show_setting( $slug ) ) {
6263
continue;
6364
}
6465

65-
$settings[ $page_type['slug'] ] = [
66-
'id' => $page_type['slug'],
66+
$settings[ $slug ] = [
67+
'id' => $slug,
6768
'value' => '_no_page_needed',
6869
'isset' => 'no',
69-
'title' => $page_type['title'],
70-
'description' => $page_type['description'] ?? '',
70+
'title' => $page_type['title'], // @phpstan-ignore offsetAccess.invalidOffset
71+
'description' => $page_type['description'] ?? '', // @phpstan-ignore offsetAccess.invalidOffset
7172
'type' => 'page-select',
72-
'page' => $page_type['slug'],
73+
'page' => $slug,
7374
];
7475

75-
if ( \progress_planner()->get_page_types()->is_page_needed( $page_type['slug'] ) ) {
76-
$type_pages = \progress_planner()->get_page_types()->get_posts_by_type( 'any', $page_type['slug'] );
76+
if ( \progress_planner()->get_page_types()->is_page_needed( $slug ) ) {
77+
$type_pages = \progress_planner()->get_page_types()->get_posts_by_type( 'any', $slug );
7778
if ( empty( $type_pages ) ) {
78-
$settings[ $page_type['slug'] ]['value'] = \progress_planner()->get_page_types()->get_default_page_id_by_type( $page_type['slug'] );
79+
$settings[ $slug ]['value'] = \progress_planner()->get_page_types()->get_default_page_id_by_type( $slug );
7980
} else {
80-
$settings[ $page_type['slug'] ]['value'] = $type_pages[0]->ID;
81-
$settings[ $page_type['slug'] ]['isset'] = 'yes';
81+
$settings[ $slug ]['value'] = $type_pages[0]->ID;
82+
$settings[ $slug ]['isset'] = 'yes';
8283

8384
// If there is more than one page, we need to check if the page has a parent with the same page-type assigned.
8485
if ( 1 < \count( $type_pages ) ) {
@@ -89,7 +90,7 @@ public function get_settings() {
8990
foreach ( $type_pages as $type_page ) {
9091
$parent = \get_post_field( 'post_parent', $type_page->ID );
9192
if ( $parent && \in_array( (int) $parent, $type_pages_ids, true ) ) {
92-
$settings[ $page_type['slug'] ]['value'] = $parent;
93+
$settings[ $slug ]['value'] = $parent;
9394
break;
9495
}
9596
}

classes/admin/widgets/class-activity-scores.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ public function get_checklist_results() {
9898
$items = $this->get_checklist();
9999
$results = [];
100100
foreach ( $items as $item ) {
101-
$results[ $item['label'] ] = $item['callback']();
101+
$label = (string) $item['label']; // @phpstan-ignore offsetAccess.invalidOffset
102+
$results[ $label ] = $item['callback'](); // @phpstan-ignore offsetAccess.invalidOffset
102103
}
103104
return $results;
104105
}

classes/class-base.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,12 @@ class Base {
8686
*/
8787
public function init() {
8888
if ( ! \function_exists( 'current_user_can' ) ) {
89-
require_once ABSPATH . 'wp-includes/capabilities.php'; // @phpstan-ignore requireOnce.fileNotFound
89+
// @phpstan-ignore-next-line requireOnce.fileNotFound
90+
require_once ABSPATH . 'wp-includes/capabilities.php';
9091
}
9192
if ( ! \function_exists( 'wp_get_current_user' ) ) {
92-
require_once ABSPATH . 'wp-includes/pluggable.php'; // @phpstan-ignore requireOnce.fileNotFound
93+
// @phpstan-ignore-next-line requireOnce.fileNotFound
94+
require_once ABSPATH . 'wp-includes/pluggable.php';
9395
}
9496

9597
if ( \defined( '\IS_PLAYGROUND_PREVIEW' ) && \constant( '\IS_PLAYGROUND_PREVIEW' ) === true ) {
@@ -421,7 +423,8 @@ public function get_file_version( $file ) {
421423

422424
// Otherwise, use the plugin header.
423425
if ( ! \function_exists( 'get_file_data' ) ) {
424-
require_once ABSPATH . 'wp-includes/functions.php'; // @phpstan-ignore requireOnce.fileNotFound
426+
// @phpstan-ignore-next-line requireOnce.fileNotFound
427+
require_once ABSPATH . 'wp-includes/functions.php';
425428
}
426429

427430
if ( ! self::$plugin_version ) {

classes/class-suggested-tasks.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -479,7 +479,9 @@ public function rest_api_tax_query( $args, $request ) {
479479

480480
// Handle sorting parameters.
481481
if ( isset( $request['filter']['orderby'] ) ) {
482-
$args['orderby'] = \sanitize_sql_orderby( $request['filter']['orderby'] );
482+
// @phpstan-ignore-next-line argument.templateType
483+
$orderby = \sanitize_sql_orderby( $request['filter']['orderby'] ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
484+
$args['orderby'] = $orderby !== false ? $orderby : 'date';
483485
}
484486
if ( isset( $request['filter']['order'] ) ) {
485487
$args['order'] = \in_array( \strtoupper( $request['filter']['order'] ), [ 'ASC', 'DESC' ], true )

classes/suggested-tasks/class-task.php

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,14 @@ public function get_rest_formatted_data( $post_id = null ): array {
200200

201201
// Make sure WP_REST_Posts_Controller is loaded.
202202
if ( ! \class_exists( 'WP_REST_Posts_Controller' ) ) {
203-
require_once ABSPATH . 'wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php'; // @phpstan-ignore requireOnce.fileNotFound
203+
// @phpstan-ignore-next-line requireOnce.fileNotFound
204+
require_once ABSPATH . 'wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php';
204205
}
205206

206207
// Make sure WP_REST_Request is loaded.
207208
if ( ! \class_exists( 'WP_REST_Request' ) ) {
208-
require_once ABSPATH . 'wp-includes/rest-api/class-wp-rest-request.php'; // @phpstan-ignore requireOnce.fileNotFound
209+
// @phpstan-ignore-next-line requireOnce.fileNotFound
210+
require_once ABSPATH . 'wp-includes/rest-api/class-wp-rest-request.php';
209211
}
210212

211213
// Use the appropriate controller for the post type.

classes/suggested-tasks/data-collector/class-inactive-plugins.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public function update_inactive_plugins_cache() {
4747
*/
4848
protected function calculate_data() {
4949
if ( ! \function_exists( 'get_plugins' ) ) {
50-
require_once ABSPATH . 'wp-admin/includes/plugin.php'; // @phpstan-ignore requireOnce.fileNotFound
50+
// @phpstan-ignore-next-line requireOnce.fileNotFound
51+
require_once ABSPATH . 'wp-admin/includes/plugin.php';
5152
}
5253

5354
// Clear the plugins cache, so get_plugins() returns the latest plugins.

0 commit comments

Comments
 (0)