api-test: replace sleeps with WaitHelper poll for async state (#3239)

* api-test: replace sleeps with WaitHelper poll for async state

* php-style
This commit is contained in:
Viktor Scharf authored and GitHub committed 2026-08-05 09:23:57 +02:00
1 parent 799f98339e
commit 76ff13b47e
4 files changed
+117 -55

No files matched your search

@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/**
* @author Viktor Scharf OpenCloud GmbH <v.scharf@opencloud.eu>
* @copyright Copyright (c) 2026 OpenCloud GmbH <v.scharf@opencloud.eu>
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License,
* as published by the Free Software Foundation;
* either version 3 of the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace TestHelpers;
/**
* Helper for waiting on asynchronous/eventually-consistent server state.
*
* @package TestHelpers
*/
class WaitHelper {
/**
* Overall time to keep polling before giving up (seconds).
*/
public const TIMEOUT_SECONDS = 10;
/**
* Pause between two attempts (milliseconds).
*/
public const INTERVAL_MS = 500;
/**
* Repeat $makeAttempt until $shouldStop returns true or the timeout elapses.
*
* @param callable $makeAttempt makes one attempt (e.g. sends a request) and returns its result
* @param callable $shouldStop receives that result, returns true to stop polling
*
* @return mixed the last result from $makeAttempt
*/
public static function waitUntil(callable $makeAttempt, callable $shouldStop): mixed {
$deadline = \microtime(true) + self::TIMEOUT_SECONDS;
$result = $makeAttempt();
while (!$shouldStop($result) && \microtime(true) < $deadline) {
\usleep(self::INTERVAL_MS * 1000);
$result = $makeAttempt();
}
return $result;
}
}
+20 -25
View File
@@ -18,6 +18,7 @@ use TestHelpers\WebDavHelper;
use TestHelpers\HttpRequestHelper;
use TestHelpers\BehatHelper;
use TestHelpers\TokenHelper;
use TestHelpers\WaitHelper;
use Behat\Step\Given;
use Behat\Step\Then;
use Behat\Step\When;
@@ -2650,40 +2651,34 @@ class GraphContext implements Context {
$credentials = $this->getAdminOrUserCredentials($user);
// Sometimes listing shares might not return the updated shares list
// so try again until @client.synchronize is true for the max. number of retries (i.e. 10)
// and do not retry when the share is expected to be not synced
// Sometimes listing shares might not return the updated shares list, so poll
// until every share reports @client.synchronize (auto-synced). Do not wait
// when retry is disabled or when the user has auto-sync turned off, i.e. when
// the share is expected to be not synced.
$retryEnabled = ($retryOption === '');
$tryAgain = false;
$retried = 0;
do {
$response = GraphHelper::getSharesSharedWithMe(
$response = WaitHelper::waitUntil(
fn () => GraphHelper::getSharesSharedWithMe(
$this->featureContext->getBaseUrl(),
$this->featureContext->getStepLineRef(),
$credentials['username'],
$credentials['password']
);
$jsonBody = $this->featureContext->getJsonDecodedResponseBodyContent($response);
if ($retryEnabled) {
),
function ($response) use ($retryEnabled, $credentials) {
if (!$retryEnabled) {
return true;
}
if (!$this->featureContext->getUserAutoSyncSetting($credentials['username'])) {
return true;
}
$jsonBody = $this->featureContext->getJsonDecodedResponseBodyContent($response);
foreach ($jsonBody->value as $share) {
$autoSync = $this->featureContext->getUserAutoSyncSetting($credentials['username']);
$tryAgain = !$share->{'@client.synchronize'}
&& $autoSync
&& $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "auto-sync share for user '$user' is enabled\n";
echo "but share '$share->name' was not auto-synced, retrying ($retried)...\n";
// wait 500ms and try again
\usleep(500 * 1000);
break;
if (!$share->{'@client.synchronize'}) {
return false;
}
}
return true;
}
} while ($tryAgain);
);
$this->featureContext->setResponse($response);
$this->featureContext->pushToLastStatusCodesArrays();
+36 -14
View File
@@ -32,6 +32,7 @@ use TestHelpers\WebDavHelper;
use TestHelpers\GraphHelper;
use TestHelpers\OcHelper;
use TestHelpers\BehatHelper;
use TestHelpers\WaitHelper;
use Behat\Step\Given;
use Behat\Step\Then;
use Behat\Step\When;
@@ -3856,15 +3857,26 @@ class SpacesContext implements Context {
string $spaceName,
TableNode $propertiesTable
): void {
// NOTE: extracting properties occurs asynchronously
// short wait is necessary before getting those properties
sleep(2);
// NOTE: extracting properties occurs asynchronously after upload, so we need to wait until the properties are available
$spaceId = $this->getSpaceIdByName($user, $spaceName);
$response = $this->webDavPropertiesContext->getPropertiesOfFolder(
$user,
$resourceName,
$spaceId,
$propertiesTable
$response = WaitHelper::waitUntil(
fn () => $this->webDavPropertiesContext->getPropertiesOfFolder(
$user,
$resourceName,
$spaceId,
$propertiesTable
),
function ($response) {
// check if the response body contains any of the extracted-property leaf names
$body = (string) $response->getBody();
$response->getBody()->rewind();
foreach (["camera-make", "latitude", "longitude", "album", "artist", "width", "height"] as $leaf) {
if (\str_contains($body, $leaf)) {
return true;
}
}
return false;
}
);
$this->featureContext->setResponse($response);
}
@@ -4544,16 +4556,26 @@ class SpacesContext implements Context {
$itemId = $this->getFileId($user, $space, $file);
}
$url = $this->featureContext->getBaseUrl() . "/graph/v1.0/drives/$spaceId/items/$itemId";
// NOTE: extracting properties occurs asynchronously
// short wait is necessary before getting those properties
sleep(2);
$this->featureContext->setResponse(
HttpRequestHelper::get(
// NOTE: extracting properties occurs asynchronously after upload, so we need to wait until the properties are available
$extractionFacets = ["image", "photo", "location", "audio", "video"];
$response = WaitHelper::waitUntil(
fn () => HttpRequestHelper::get(
$url,
$this->featureContext->getStepLineRef(),
$user,
$this->featureContext->getPasswordForUser($user),
)
),
function ($response) use ($extractionFacets) {
if ($response->getStatusCode() !== 200) {
return true;
}
$body = $this->featureContext->getJsonDecodedResponseBodyContent($response);
return \is_object($body)
&& !empty(\array_intersect($extractionFacets, \array_keys((array) $body)));
}
);
$this->featureContext->setResponse($response);
}
}
+5 -16
View File
@@ -33,6 +33,7 @@ use TestHelpers\HttpRequestHelper;
use TestHelpers\WebDavHelper;
use TestHelpers\BehatHelper;
use TestHelpers\UploadHelper;
use TestHelpers\WaitHelper;
use Behat\Step\Given;
use Behat\Step\When;
@@ -238,22 +239,10 @@ class TUSContext implements Context {
): void {
$resourceLocation = $this->getLastTusResourceLocation();
$retried = 0;
do {
$tryAgain = false;
$response = $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data);
// retry on 409 Conflict (Offset mismatch during TUS upload)
if ($response->getStatusCode() === 409) {
$tryAgain = true;
}
$tryAgain = $tryAgain && $retried < HttpRequestHelper::numRetriesOnHttpTooEarly();
if ($tryAgain) {
$retried += 1;
echo "Offset mismatch during TUS upload, retrying ($retried)...\n";
// wait 1s and try again
\sleep(1);
}
} while ($tryAgain);
$response = WaitHelper::waitUntil(
fn () => $this->uploadChunkToTUSLocation($user, $resourceLocation, $offset, $data),
fn ($response) => $response->getStatusCode() !== 409
);
$this->featureContext->setResponse($response);
}