Build/Test Tools: Add a retry mechanism for tests that perform external HTTP requests.

While the `skipTestOnTimeout()` method will catch a timeout and prevent it from causing a test to fail, other errors such as a failed DNS lookup or HTTPS handshake can still cause a test to unnecessarily fail. This introduces a simple retry mechanism that will hopefully further reduce the flakiness of tests that perform HTTP API requests.

Fixes #62830

git-svn-id: https://develop.svn.wordpress.org/trunk@59729 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
John Blackbourn 2025-01-29 18:17:34 +00:00
parent 4863a926ef
commit 9acdbb9d8d
4 changed files with 148 additions and 66 deletions

View File

@ -1694,4 +1694,119 @@ abstract class WP_UnitTestCase_Base extends PHPUnit_Adapter_TestCase {
touch( $file );
}
/**
* Wrapper for `wp_safe_remote_request()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_safe_remote_request( $url, $args = array() ) {
return self::retry_on_error( 'wp_safe_remote_request', $url, $args );
}
/**
* Wrapper for `wp_safe_remote_get()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_safe_remote_get( $url, $args = array() ) {
return self::retry_on_error( 'wp_safe_remote_get', $url, $args );
}
/**
* Wrapper for `wp_safe_remote_post()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_safe_remote_post( $url, $args = array() ) {
return self::retry_on_error( 'wp_safe_remote_post', $url, $args );
}
/**
* Wrapper for `wp_safe_remote_head()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_safe_remote_head( $url, $args = array() ) {
return self::retry_on_error( 'wp_safe_remote_head', $url, $args );
}
/**
* Wrapper for `wp_remote_request()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_remote_request( $url, $args = array() ) {
return self::retry_on_error( 'wp_remote_request', $url, $args );
}
/**
* Wrapper for `wp_remote_get()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_remote_get( $url, $args = array() ) {
return self::retry_on_error( 'wp_remote_get', $url, $args );
}
/**
* Wrapper for `wp_remote_post()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_remote_post( $url, $args = array() ) {
return self::retry_on_error( 'wp_remote_post', $url, $args );
}
/**
* Wrapper for `wp_remote_head()` that retries on error and skips the test on timeout.
*
* @param string $url URL to retrieve.
* @param array $args Optional. Request arguments. Default empty array.
* @return array|WP_Error The response or WP_Error on failure.
*/
protected function wp_remote_head( $url, $args = array() ) {
return self::retry_on_error( 'wp_remote_head', $url, $args );
}
/**
* Retries an HTTP API request up to three times and skips the test on timeout.
*
* @param callable $callback The HTTP API request function to call.
* @param string $url URL to retrieve.
* @param array $args Request arguments.
* @return array|WP_Error The response or WP_Error on failure.
*/
private function retry_on_error( callable $callback, $url, $args ) {
$attempts = 0;
while ( $attempts < 3 ) {
$result = call_user_func( $callback, $url, $args );
if ( ! is_wp_error( $result ) ) {
return $result;
}
++$attempts;
sleep( 5 );
}
$this->skipTestOnTimeout( $result );
return $result;
}
}

View File

@ -44,9 +44,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirect_on_301() {
// 5 : 5 & 301.
$res = wp_remote_request( $this->redirection_script . '?code=301&rt=' . 5, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=301&rt=' . 5, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 200, (int) $res['response']['code'] );
}
@ -56,9 +55,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirect_on_302() {
// 5 : 5 & 302.
$res = wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 200, (int) $res['response']['code'] );
}
@ -70,9 +68,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirect_on_301_no_redirect() {
// 5 > 0 & 301.
$res = wp_remote_request( $this->redirection_script . '?code=301&rt=' . 5, array( 'redirection' => 0 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=301&rt=' . 5, array( 'redirection' => 0 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 301, (int) $res['response']['code'] );
}
@ -84,9 +81,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirect_on_302_no_redirect() {
// 5 > 0 & 302.
$res = wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 0 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 0 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 302, (int) $res['response']['code'] );
}
@ -96,9 +92,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirections_equal() {
// 5 - 5.
$res = wp_remote_request( $this->redirection_script . '?rt=' . 5, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?rt=' . 5, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 200, (int) $res['response']['code'] );
}
@ -108,9 +103,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_no_head_redirections() {
// No redirections on HEAD request.
$res = wp_remote_request( $this->redirection_script . '?code=302&rt=' . 1, array( 'method' => 'HEAD' ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=302&rt=' . 1, array( 'method' => 'HEAD' ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 302, (int) $res['response']['code'] );
}
@ -122,7 +116,7 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirect_on_head() {
// Redirections on HEAD request when Requested.
$res = wp_remote_request(
$res = $this->wp_remote_request(
$this->redirection_script . '?rt=' . 5,
array(
'redirection' => 5,
@ -130,7 +124,6 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
)
);
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 200, (int) $res['response']['code'] );
}
@ -140,9 +133,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirections_greater() {
// 10 > 5.
$res = wp_remote_request( $this->redirection_script . '?rt=' . 10, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?rt=' . 10, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertWPError( $res );
}
@ -151,9 +143,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirections_greater_edgecase() {
// 6 > 5 (close edge case).
$res = wp_remote_request( $this->redirection_script . '?rt=' . 6, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?rt=' . 6, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertWPError( $res );
}
@ -162,9 +153,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirections_less_edgecase() {
// 4 < 5 (close edge case).
$res = wp_remote_request( $this->redirection_script . '?rt=' . 4, array( 'redirection' => 5 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?rt=' . 4, array( 'redirection' => 5 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
}
@ -175,9 +165,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_redirections_zero_redirections_specified() {
// 0 redirections asked for, should return the document?
$res = wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 0 ) );
$res = $this->wp_remote_request( $this->redirection_script . '?code=302&rt=' . 5, array( 'redirection' => 0 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 302, (int) $res['response']['code'] );
}
@ -191,9 +180,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
*/
public function test_location_header_on_201() {
// Prints PASS on initial load, FAIL if the client follows the specified redirection.
$res = wp_remote_request( $this->redirection_script . '?201-location=true' );
$res = $this->wp_remote_request( $this->redirection_script . '?201-location=true' );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 'PASS', $res['body'] );
}
@ -210,7 +198,7 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
$url = 'http://api.wordpress.org/core/tests/1.0/redirection.php?201-location=1';
// Test 301 - POST to POST.
$res = wp_remote_request(
$res = $this->wp_remote_request(
$url,
array(
'method' => 'PUT',
@ -218,7 +206,6 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
)
);
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 'PASS', wp_remote_retrieve_body( $res ) );
$this->assertNotEmpty( $res['headers']['location'] );
@ -236,9 +223,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
'test2' => 0,
'test3' => '',
);
$res = wp_remote_request( $this->redirection_script . '?header-check', array( 'headers' => $headers ) );
$res = $this->wp_remote_request( $this->redirection_script . '?header-check', array( 'headers' => $headers ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$headers = array();
@ -267,7 +253,7 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
public function test_file_stream() {
$url = $this->file_stream_url;
$size = 153204;
$res = wp_remote_request(
$res = $this->wp_remote_request(
$url,
array(
'stream' => true,
@ -281,7 +267,6 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
unlink( $res['filename'] );
}
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( '', $res['body'] ); // The body should be empty.
$this->assertEquals( $size, $res['headers']['Content-Length'] ); // Check the headers are returned (and the size is the same).
@ -297,7 +282,7 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
public function test_file_stream_limited_size() {
$url = $this->file_stream_url;
$size = 10000;
$res = wp_remote_request(
$res = $this->wp_remote_request(
$url,
array(
'stream' => true,
@ -312,7 +297,6 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
unlink( $res['filename'] );
}
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( $size, $filesize ); // Check that the file is written to disk correctly without any extra characters.
}
@ -328,7 +312,7 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
$url = $this->file_stream_url;
$size = 10000;
$res = wp_remote_request(
$res = $this->wp_remote_request(
$url,
array(
'timeout' => 30,
@ -336,7 +320,6 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
)
);
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( $size, strlen( $res['body'] ) );
}
@ -354,9 +337,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
public function test_post_redirect_to_method_300( $response_code, $method ) {
$url = 'http://api.wordpress.org/core/tests/1.0/redirection.php?post-redirect-to-method=1';
$res = wp_remote_post( add_query_arg( 'response_code', $response_code, $url ), array( 'timeout' => 30 ) );
$res = $this->wp_remote_post( add_query_arg( 'response_code', $response_code, $url ), array( 'timeout' => 30 ) );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( $method, wp_remote_retrieve_body( $res ) );
}
@ -405,9 +387,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
'redirection' => 0,
);
$res = wp_remote_get( $url, $args );
$res = $this->wp_remote_get( $url, $args );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 'PASS', wp_remote_retrieve_body( $res ) );
}
@ -427,11 +408,10 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
add_filter( 'http_request_args', array( $this, 'filter_http_request_args' ) );
$res = wp_remote_head( $url, $args );
$res = $this->wp_remote_head( $url, $args );
remove_filter( 'http_request_args', array( $this, 'filter_http_request_args' ) );
$this->skipTestOnTimeout( $res );
$this->assertNotEmpty( $this->http_request_args['sslcertificates'] );
$this->assertNotWPError( $res );
}
@ -447,9 +427,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
public function test_cookie_handling() {
$url = 'http://api.wordpress.org/core/tests/1.0/redirection.php?cookie-test=1';
$res = wp_remote_get( $url );
$res = $this->wp_remote_get( $url );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
$this->assertSame( 'PASS', wp_remote_retrieve_body( $res ) );
}
@ -467,9 +446,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
$this->fail( 'This installation of PHP does not support SSL.' );
}
$res = wp_remote_get( 'https://wordpress.org/' );
$res = $this->wp_remote_get( 'https://wordpress.org/' );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
}
@ -484,9 +462,8 @@ abstract class WP_HTTP_UnitTestCase extends WP_UnitTestCase {
$path = parse_url( $url, PHP_URL_PATH );
$url = str_replace( $path, '/' . $path, $url );
$res = wp_remote_request( $url );
$res = $this->wp_remote_request( $url );
$this->skipTestOnTimeout( $res );
$this->assertNotWPError( $res );
}
}

View File

@ -12,9 +12,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_head_request() {
// This URL gives a direct 200 response.
$url = 'https://s.w.org/screenshots/3.9/dashboard.png';
$response = wp_remote_head( $url );
$response = $this->wp_remote_head( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$headers = wp_remote_retrieve_headers( $response );
@ -31,9 +30,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_head_redirect() {
// This URL will 301 redirect.
$url = 'https://wp.org/screenshots/3.9/dashboard.png';
$response = wp_remote_head( $url );
$response = $this->wp_remote_head( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$this->assertSame( 301, wp_remote_retrieve_response_code( $response ) );
}
@ -43,9 +41,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
*/
public function test_head_404() {
$url = 'https://wordpress.org/screenshots/3.9/awefasdfawef.jpg';
$response = wp_remote_head( $url );
$response = $this->wp_remote_head( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$this->assertSame( 404, wp_remote_retrieve_response_code( $response ) );
}
@ -58,9 +55,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_get_request() {
$url = 'https://s.w.org/screenshots/3.9/dashboard.png';
$response = wp_remote_get( $url );
$response = $this->wp_remote_get( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$headers = wp_remote_retrieve_headers( $response );
@ -80,9 +76,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
// This will redirect to wordpress.org.
$url = 'https://wp.org/screenshots/3.9/dashboard.png';
$response = wp_remote_get( $url );
$response = $this->wp_remote_get( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$headers = wp_remote_retrieve_headers( $response );
@ -101,9 +96,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
$url = 'https://wp.org/screenshots/3.9/dashboard.png';
// Pretend we've already redirected 5 times.
$response = wp_remote_get( $url, array( 'redirection' => -1 ) );
$response = $this->wp_remote_get( $url, array( 'redirection' => -1 ) );
$this->skipTestOnTimeout( $response );
$this->assertWPError( $response );
}
@ -118,9 +112,8 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_get_response_cookies() {
$url = 'https://login.wordpress.org/wp-login.php';
$response = wp_remote_head( $url );
$response = $this->wp_remote_head( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$cookies = wp_remote_retrieve_cookies( $response );
@ -152,7 +145,7 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_get_response_cookies_with_wp_http_cookie_object() {
$url = 'https://login.wordpress.org/wp-login.php';
$response = wp_remote_get(
$response = $this->wp_remote_get(
$url,
array(
'cookies' => array(
@ -166,7 +159,6 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
)
);
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$cookies = wp_remote_retrieve_cookies( $response );
@ -189,7 +181,7 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
public function test_get_response_cookies_with_name_value_array() {
$url = 'https://login.wordpress.org/wp-login.php';
$response = wp_remote_get(
$response = $this->wp_remote_get(
$url,
array(
'cookies' => array(
@ -198,7 +190,6 @@ class Tests_HTTP_Functions extends WP_UnitTestCase {
)
);
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$cookies = wp_remote_retrieve_cookies( $response );

View File

@ -91,9 +91,8 @@ class Tests_Readme extends WP_UnitTestCase {
* @return string The response body.
*/
public function get_response_body( $url ) {
$response = wp_remote_get( $url );
$response = $this->wp_remote_get( $url );
$this->skipTestOnTimeout( $response );
$this->assertNotWPError( $response );
$response_code = wp_remote_retrieve_response_code( $response );