Skip to content

Commit 6a7de00

Browse files
authored
PHPUnit: Fix slow tests (#1087)
* Tests: Fix slow tests Takes tests execution from 7.8s to ~3s. * Fix phpcs * Try comma separation * Remove type hint * Don't listen by default for as long as we support 7.0 * Mock response instead of bypassing it entirely * More specific error message * Use existing fixtures
1 parent 9011ffc commit 6a7de00

File tree

5 files changed

+312
-26
lines changed

5 files changed

+312
-26
lines changed

phpunit.xml.dist

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,9 @@
1111
<directory prefix="class-test-" suffix=".php">./tests</directory>
1212
</testsuite>
1313
</testsuites>
14+
<!--
15+
<listeners>
16+
<listener class="Activitypub\Tests\Activitypub_Testcase_Timer" file="tests/class-activitypub-testcase-timer.php" />
17+
</listeners>
18+
-->
1419
</phpunit>

tests/bootstrap.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
\define( 'WP_SITEURL', 'http://example.org' );
1313
\define( 'WP_HOME', 'http://example.org' );
1414

15+
\define( 'AP_TESTS_DIR', __DIR__ );
1516
$_tests_dir = \getenv( 'WP_TESTS_DIR' );
1617

1718
if ( ! $_tests_dir ) {
@@ -38,6 +39,68 @@ function _manually_load_plugin() {
3839
}
3940
\tests_add_filter( 'muplugins_loaded', '_manually_load_plugin' );
4041

42+
/**
43+
* Disable HTTP requests.
44+
*
45+
* @param mixed $response The value to return instead of making a HTTP request.
46+
* @param array $args Request arguments.
47+
* @param string $url The request URL.
48+
* @return mixed|false|WP_Error
49+
*/
50+
function http_disable_request( $response, $args, $url ) {
51+
if ( false !== $response ) {
52+
// Another filter has already overridden this request.
53+
return $response;
54+
}
55+
56+
/**
57+
* Allow HTTP requests to be made.
58+
*
59+
* @param bool $allow Whether to allow the HTTP request.
60+
* @param array $args Request arguments.
61+
* @param string $url The request URL.
62+
*/
63+
if ( apply_filters( 'tests_allow_http_request', false, $args, $url ) ) {
64+
// This request has been specifically permitted.
65+
return false;
66+
}
67+
68+
$backtrace = array_reverse( debug_backtrace() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_debug_backtrace,PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue.NeedsInspection
69+
$trace_str = '';
70+
foreach ( $backtrace as $frame ) {
71+
if (
72+
( isset( $frame['file'] ) && strpos( $frame['file'], 'phpunit.php' ) !== false ) ||
73+
( isset( $frame['file'] ) && strpos( $frame['file'], 'wp-includes/http.php' ) !== false ) ||
74+
( isset( $frame['file'] ) && strpos( $frame['file'], 'wp-includes/class-wp-hook.php' ) !== false ) ||
75+
( isset( $frame['function'] ) && __FUNCTION__ === $frame['function'] ) ||
76+
( isset( $frame['function'] ) && 'apply_filters' === $frame['function'] )
77+
) {
78+
continue;
79+
}
80+
81+
if ( $trace_str ) {
82+
$trace_str .= ', ';
83+
}
84+
85+
if ( ! empty( $frame['file'] ) && ! empty( $frame['line'] ) ) {
86+
$trace_str .= basename( $frame['file'] ) . ':' . $frame['line'];
87+
if ( ! empty( $frame['function'] ) ) {
88+
$trace_str .= ' ';
89+
}
90+
}
91+
92+
if ( ! empty( $frame['function'] ) ) {
93+
if ( ! empty( $frame['class'] ) ) {
94+
$trace_str .= $frame['class'] . '::';
95+
}
96+
$trace_str .= $frame['function'] . '()';
97+
}
98+
}
99+
100+
return new WP_Error( 'cancelled', 'Live HTTP request cancelled by bootstrap.php' );
101+
}
102+
\tests_add_filter( 'pre_http_request', 'http_disable_request', 99, 3 );
103+
41104
// Start up the WP testing environment.
42105
require $_tests_dir . '/includes/bootstrap.php';
43106
require __DIR__ . '/class-activitypub-testcase-cache-http.php';
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
/**
3+
* Test Timer Listener for PHPUnit.
4+
*
5+
* phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped,PHPCompatibility.FunctionDeclarations.NewReturnTypeDeclarations.voidFound
6+
*
7+
* @package Activitypub
8+
*/
9+
10+
namespace Activitypub\Tests;
11+
12+
use PHPUnit\Framework\TestListener;
13+
use PHPUnit\Framework\TestListenerDefaultImplementation;
14+
use PHPUnit\Framework\Test;
15+
use PHPUnit\Framework\TestSuite;
16+
17+
/**
18+
* Activitypub Testcase Timer class.
19+
*/
20+
class Activitypub_Testcase_Timer implements TestListener {
21+
use TestListenerDefaultImplementation;
22+
23+
/**
24+
* Store test start times.
25+
*
26+
* @var array
27+
*/
28+
private $test_start_times = array();
29+
30+
/**
31+
* Store slow tests.
32+
*
33+
* @var array
34+
*/
35+
private $slow_tests = array();
36+
37+
/**
38+
* Threshold for slow tests in seconds.
39+
*
40+
* @var float
41+
*/
42+
private $slow_threshold = 0.2; // 200ms
43+
44+
/**
45+
* A test started.
46+
*
47+
* @param Test $test The test case.
48+
*/
49+
public function startTest( Test $test ): void {
50+
$this->test_start_times[ $test->getName() ] = microtime( true );
51+
}
52+
53+
/**
54+
* A test ended.
55+
*
56+
* @param Test $test The test case.
57+
* @param float $time Time taken.
58+
*/
59+
public function endTest( Test $test, $time ): void { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
60+
$test_name = $test->getName();
61+
if ( ! isset( $this->test_start_times[ $test_name ] ) ) {
62+
return;
63+
}
64+
65+
$duration = microtime( true ) - $this->test_start_times[ $test_name ];
66+
if ( $duration >= $this->slow_threshold ) {
67+
$this->slow_tests[] = array(
68+
'name' => sprintf( '%s::%s', get_class( $test ), $test_name ),
69+
'duration' => $duration,
70+
);
71+
}
72+
73+
unset( $this->test_start_times[ $test_name ] );
74+
}
75+
76+
/**
77+
* A test suite ended.
78+
*
79+
* @param TestSuite $suite The test suite.
80+
*/
81+
public function endTestSuite( TestSuite $suite ): void {
82+
if ( $suite->getName() === 'ActivityPub' && ! empty( $this->slow_tests ) ) {
83+
usort(
84+
$this->slow_tests,
85+
function ( $a, $b ) {
86+
return $b['duration'] <=> $a['duration'];
87+
}
88+
);
89+
90+
echo "\n\nSlow Tests (>= {$this->slow_threshold}s):\n";
91+
foreach ( $this->slow_tests as $test ) {
92+
printf(
93+
" \033[33m%.3fs\033[0m %s\n",
94+
$test['duration'],
95+
$test['name']
96+
);
97+
}
98+
echo "\n";
99+
}
100+
}
101+
}

tests/includes/class-test-mention.php

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,24 @@ class Test_Mention extends \WP_UnitTestCase {
2929
),
3030
);
3131

32+
/**
33+
* Set up the test case.
34+
*/
35+
public function set_up() {
36+
parent::set_up();
37+
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
38+
add_filter( 'pre_http_request', array( $this, 'pre_http_request' ), 10, 3 );
39+
}
40+
41+
/**
42+
* Tear down the test case.
43+
*/
44+
public function tear_down() {
45+
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
46+
remove_filter( 'pre_http_request', array( $this, 'pre_http_request' ) );
47+
parent::tear_down();
48+
}
49+
3250
/**
3351
* Test the content.
3452
*
@@ -39,11 +57,7 @@ class Test_Mention extends \WP_UnitTestCase {
3957
* @param string $content_with_mention The content with mention.
4058
*/
4159
public function test_the_content( $content, $content_with_mention ) {
42-
add_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ), 10, 2 );
43-
$content = Mention::the_content( $content );
44-
remove_filter( 'pre_get_remote_metadata_by_actor', array( get_called_class(), 'pre_get_remote_metadata_by_actor' ) );
45-
46-
$this->assertEquals( $content_with_mention, $content );
60+
$this->assertEquals( $content_with_mention, Mention::the_content( $content ) );
4761
}
4862

4963
/**
@@ -77,17 +91,52 @@ public function the_content_provider() {
7791
}
7892

7993
/**
80-
* Filter for get_remote_metadata_by_actor.
94+
* Mock HTTP requests.
95+
*
96+
* @param false|array|\WP_Error $response HTTP response.
97+
* @param array $parsed_args HTTP request arguments.
98+
* @param string $url The request URL.
99+
* @return array|false|\WP_Error
100+
*/
101+
public function pre_http_request( $response, $parsed_args, $url ) {
102+
// Mock responses for remote users.
103+
if ( 'https://notiz.blog/.well-known/webfinger?resource=acct%3Apfefferle%40notiz.blog' === $url ) {
104+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
105+
return json_decode( file_get_contents( AP_TESTS_DIR . '/fixtures/notiz-blog-well-known-webfinger.json' ), true );
106+
}
107+
108+
if ( 'https://lemmy.ml/.well-known/webfinger?resource=acct%3Apfefferle%40lemmy.ml' === $url ) {
109+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
110+
return json_decode( file_get_contents( AP_TESTS_DIR . '/fixtures/lemmy-ml-well-known-webfinger.json' ), true );
111+
}
112+
113+
if ( 'https://notiz.blog/author/matthias-pfefferle/' === $url ) {
114+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
115+
return json_decode( file_get_contents( AP_TESTS_DIR . '/fixtures/notiz-blog-author-matthias-pfefferle.json' ), true );
116+
}
117+
118+
if ( 'https://lemmy.ml/u/pfefferle' === $url ) {
119+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
120+
return json_decode( file_get_contents( AP_TESTS_DIR . '/fixtures/lemmy-ml-u-pfefferle.json' ), true );
121+
}
122+
123+
return $response;
124+
}
125+
126+
/**
127+
* Filters remote metadata by actor.
81128
*
82-
* @param string $pre The pre.
83-
* @param string $actor The actor.
84-
* @return array
129+
* @param array|string $pre The pre-filtered value.
130+
* @param string $actor The actor.
131+
* @return array|string
85132
*/
86133
public static function pre_get_remote_metadata_by_actor( $pre, $actor ) {
87134
$actor = ltrim( $actor, '@' );
135+
88136
if ( isset( self::$users[ $actor ] ) ) {
89137
return self::$users[ $actor ];
90138
}
139+
91140
return $pre;
92141
}
93142
}

0 commit comments

Comments
 (0)