Merge pull request #8682 from appwrite/fix-flaky-tests

fix: flaky functions tests
This commit is contained in:
Matej Bačo 2024-10-03 11:07:40 +02:00 committed by GitHub
commit a6ceda3543
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1139 additions and 2469 deletions

View file

@ -16,22 +16,22 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
- name: Build Appwrite
uses: docker/build-push-action@v3
uses: docker/build-push-action@v6
with:
context: .
push: false
tags: ${{ env.IMAGE }}
load: true
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=appwrite
cache-to: type=gha,mode=max,scope=appwrite
outputs: type=docker,dest=/tmp/${{ env.IMAGE }}.tar
build-args: |
DEBUG=false
@ -39,9 +39,11 @@ jobs:
VERSION=dev
- name: Cache Docker Image
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
restore-keys: |
appwrite-dev-
path: /tmp/${{ env.IMAGE }}.tar
unit_test:
@ -51,10 +53,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -81,10 +83,10 @@ jobs:
needs: setup
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -113,6 +115,7 @@ jobs:
Console,
Databases,
Functions,
FunctionsSchedule,
GraphQL,
Health,
Locale,
@ -128,10 +131,10 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar
@ -141,7 +144,7 @@ jobs:
run: |
docker load --input /tmp/${{ env.IMAGE }}.tar
docker compose up -d
sleep 25
sleep 30
- name: Run ${{matrix.service}} Tests
run: docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
@ -149,15 +152,15 @@ jobs:
- name: Run ${{matrix.service}} Shared Tables Tests
run: _APP_DATABASE_SHARED_TABLES=database_db_main docker compose exec -T appwrite test /usr/src/code/tests/e2e/Services/${{matrix.service}} --debug
benchamrking:
benchmarking:
name: Benchmark
runs-on: ubuntu-latest
needs: setup
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Load Cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
key: ${{ env.CACHE_KEY }}
path: /tmp/${{ env.IMAGE }}.tar

View file

@ -883,9 +883,6 @@ class UsageTest extends Scope
$this->assertEquals(201, $response['headers']['status-code']);
$this->assertNotEmpty($response['body']['$id']);
$code = realpath(__DIR__ . '/../../resources/functions') . "/php/code.tar.gz";
$this->packageCode('php');
$response = $this->client->call(
Client::METHOD_POST,
'/functions/' . $functionId . '/deployments',
@ -895,8 +892,8 @@ class UsageTest extends Scope
], $this->getHeaders()),
[
'entrypoint' => 'index.php',
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
'activate' => true
'code' => $this->packageFunction('php'),
'activate' => true,
]
);
@ -934,7 +931,7 @@ class UsageTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'async' => false,
'async' => 'false',
]
);
@ -958,7 +955,7 @@ class UsageTest extends Scope
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()),
[
'async' => false,
'async' => 'false',
]
);

View file

@ -2,234 +2,207 @@
namespace Tests\E2E\Services\Functions;
use Appwrite\Tests\Async;
use CURLFile;
use Tests\E2E\Client;
use Utopia\CLI\Console;
trait FunctionsBase
{
use Async;
protected string $stdout = '';
protected string $stderr = '';
protected function packageCode($folder)
protected function setupFunction(mixed $params): string
{
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), $params);
$this->assertEquals($function['headers']['status-code'], 201, 'Setup function failed with status code: ' . $function['headers']['status-code'] . ' and response: ' . json_encode($function['body'], JSON_PRETTY_PRINT));
$functionId = $function['body']['$id'];
return $functionId;
}
protected function awaitDeploymentIsBuilt($functionId, $deploymentId, $checkForSuccess = true): void
protected function setupDeployment(string $functionId, mixed $params): string
{
while (true) {
$deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, [
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]), $params);
$this->assertEquals($deployment['headers']['status-code'], 202, 'Setup deployment failed with status code: ' . $deployment['headers']['status-code'] . ' and response: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEventually(function () use ($functionId, $deploymentId) {
$deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
]));
$this->assertEquals('ready', $deployment['body']['status'], 'Deployment status is not ready, deployment: ' . json_encode($deployment['body'], JSON_PRETTY_PRINT));
}, 50000, 500);
if (
$deployment['headers']['status-code'] >= 400
|| \in_array($deployment['body']['status'], ['ready', 'failed'])
) {
break;
}
\sleep(1);
}
if ($checkForSuccess) {
$this->assertEquals(200, $deployment['headers']['status-code']);
$this->assertEquals('ready', $deployment['body']['status'], \json_encode($deployment['body']));
}
return $deploymentId;
}
// /**
// * @depends testCreateTeam
// */
// public function testGetTeam($data):array
// {
// $id = $data['teamUid'] ?? '';
protected function cleanupFunction(string $functionId): void
{
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]));
// /**
// * Test for SUCCESS
// */
// $response = $this->client->call(Client::METHOD_GET, '/teams/'.$id, array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()));
$this->assertEquals($function['headers']['status-code'], 204);
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertNotEmpty($response['body']['$id']);
// $this->assertEquals('Arsenal', $response['body']['name']);
// $this->assertGreaterThan(-1, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertIsInt($response['body']['dateCreated']);
protected function createFunction(mixed $params): mixed
{
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// /**
// * Test for FAILURE
// */
return $function;
}
// return [];
// }
protected function createVariable(string $functionId, mixed $params): mixed
{
$variable = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// /**
// * @depends testCreateTeam
// */
// public function testListTeams($data):array
// {
// /**
// * Test for SUCCESS
// */
// $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()));
return $variable;
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertGreaterThan(0, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertCount(3, $response['body']['teams']);
protected function getFunction(string $functionId): mixed
{
$function = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'limit' => 2,
// ]);
return $function;
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertGreaterThan(0, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertCount(2, $response['body']['teams']);
protected function getDeployment(string $functionId, string $deploymentId): mixed
{
$deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'offset' => 1,
// ]);
return $deployment;
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertGreaterThan(0, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertCount(2, $response['body']['teams']);
protected function getExecution(string $functionId, $executionId): mixed
{
$execution = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'search' => 'Manchester',
// ]);
return $execution;
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertGreaterThan(0, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertCount(1, $response['body']['teams']);
// $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']);
protected function listFunctions(mixed $params = []): mixed
{
$functions = $this->client->call(Client::METHOD_GET, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// $response = $this->client->call(Client::METHOD_GET, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'search' => 'United',
// ]);
return $functions;
}
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertGreaterThan(0, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertCount(1, $response['body']['teams']);
// $this->assertEquals('Manchester United', $response['body']['teams'][0]['name']);
protected function listDeployments(string $functionId, $params = []): mixed
{
$deployments = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// /**
// * Test for FAILURE
// */
return $deployments;
}
// return [];
// }
protected function listExecutions(string $functionId, mixed $params = []): mixed
{
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// public function testUpdateTeam():array
// {
// /**
// * Test for SUCCESS
// */
// $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'name' => 'Demo'
// ]);
return $executions;
}
// $this->assertEquals(201, $response['headers']['status-code']);
// $this->assertNotEmpty($response['body']['$id']);
// $this->assertEquals('Demo', $response['body']['name']);
// $this->assertGreaterThan(-1, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertIsInt($response['body']['dateCreated']);
protected function packageFunction(string $function): CURLFile
{
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
// $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'name' => 'Demo New'
// ]);
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
// $this->assertEquals(200, $response['headers']['status-code']);
// $this->assertNotEmpty($response['body']['$id']);
// $this->assertEquals('Demo New', $response['body']['name']);
// $this->assertGreaterThan(-1, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertIsInt($response['body']['dateCreated']);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
}
// /**
// * Test for FAILURE
// */
// $response = $this->client->call(Client::METHOD_PUT, '/teams/'.$response['body']['$id'], array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// ]);
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
// $this->assertEquals(400, $response['headers']['status-code']);
protected function createDeployment(string $functionId, mixed $params = []): mixed
{
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// return [];
// }
return $deployment;
}
// public function testDeleteTeam():array
// {
// /**
// * Test for SUCCESS
// */
// $response = $this->client->call(Client::METHOD_POST, '/teams', array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()), [
// 'name' => 'Demo'
// ]);
protected function getFunctionUsage(string $functionId, mixed $params): mixed
{
$usage = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// $teamUid = $response['body']['$id'];
return $usage;
}
// $this->assertEquals(201, $response['headers']['status-code']);
// $this->assertNotEmpty($response['body']['$id']);
// $this->assertEquals('Demo', $response['body']['name']);
// $this->assertGreaterThan(-1, $response['body']['total']);
// $this->assertIsInt($response['body']['total']);
// $this->assertIsInt($response['body']['dateCreated']);
protected function getTemplate(string $templateId)
{
$template = $this->client->call(Client::METHOD_GET, '/functions/templates/' . $templateId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// $response = $this->client->call(Client::METHOD_DELETE, '/teams/'.$teamUid, array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()));
return $template;
}
// $this->assertEquals(204, $response['headers']['status-code']);
// $this->assertEmpty($response['body']);
protected function createExecution(string $functionId, mixed $params = []): mixed
{
$execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), $params);
// /**
// * Test for FAILURE
// */
// $response = $this->client->call(Client::METHOD_GET, '/teams/'.$teamUid, array_merge([
// 'content-type' => 'application/json',
// 'x-appwrite-project' => $this->getProject()['$id'],
// ], $this->getHeaders()));
return $execution;
}
// $this->assertEquals(404, $response['headers']['status-code']);
protected function deleteFunction(string $functionId): mixed
{
$function = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
// return [];
// }
return $function;
}
}

View file

@ -13,13 +13,11 @@ class FunctionsConsoleClientTest extends Scope
{
use ProjectCustom;
use SideConsole;
use FunctionsBase;
public function testCreateFunction(): array
{
$function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$function = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
@ -35,10 +33,9 @@ class FunctionsConsoleClientTest extends Scope
$this->assertEquals(201, $function['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/functions', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
$functionId = $function['body']['$id'];
$function2 = $this->createFunction([
'functionId' => ID::unique(),
'name' => 'Test Failure',
'execute' => ['some-random-string'],
@ -46,73 +43,59 @@ class FunctionsConsoleClientTest extends Scope
'entrypoint' => 'index.php',
]);
$this->assertEquals(400, $response['headers']['status-code']);
$this->assertEquals(400, $function2['headers']['status-code']);
return [
'functionId' => $function['body']['$id']
'functionId' => $functionId,
];
}
/**
* @depends testCreateFunction
*/
public function testGetCollectionUsage(array $data)
public function testFunctionUsage(array $data)
{
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '232h'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_GET, '/functions/randomFunctionId/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
'range' => '24h'
]);
$this->assertEquals(404, $response['headers']['status-code']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/functions/' . $data['functionId'] . '/usage', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id']
], $this->getHeaders()), [
$usage = $this->getFunctionUsage($data['functionId'], [
'range' => '24h'
]);
$this->assertEquals(200, $usage['headers']['status-code']);
$this->assertEquals(19, count($usage['body']));
$this->assertEquals('24h', $usage['body']['range']);
$this->assertIsNumeric($usage['body']['deploymentsTotal']);
$this->assertIsNumeric($usage['body']['deploymentsStorageTotal']);
$this->assertIsNumeric($usage['body']['buildsTotal']);
$this->assertIsNumeric($usage['body']['buildsStorageTotal']);
$this->assertIsNumeric($usage['body']['buildsTimeTotal']);
$this->assertIsNumeric($usage['body']['buildsMbSecondsTotal']);
$this->assertIsNumeric($usage['body']['executionsTotal']);
$this->assertIsNumeric($usage['body']['executionsTimeTotal']);
$this->assertIsNumeric($usage['body']['executionsMbSecondsTotal']);
$this->assertIsArray($usage['body']['deployments']);
$this->assertIsArray($usage['body']['deploymentsStorage']);
$this->assertIsArray($usage['body']['builds']);
$this->assertIsArray($usage['body']['buildsTime']);
$this->assertIsArray($usage['body']['buildsStorage']);
$this->assertIsArray($usage['body']['buildsTime']);
$this->assertIsArray($usage['body']['buildsMbSeconds']);
$this->assertIsArray($usage['body']['executions']);
$this->assertIsArray($usage['body']['executionsTime']);
$this->assertIsArray($usage['body']['executionsMbSeconds']);
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(19, count($response['body']));
$this->assertEquals('24h', $response['body']['range']);
$this->assertIsNumeric($response['body']['deploymentsTotal']);
$this->assertIsNumeric($response['body']['deploymentsStorageTotal']);
$this->assertIsNumeric($response['body']['buildsTotal']);
$this->assertIsNumeric($response['body']['buildsStorageTotal']);
$this->assertIsNumeric($response['body']['buildsTimeTotal']);
$this->assertIsNumeric($response['body']['buildsMbSecondsTotal']);
$this->assertIsNumeric($response['body']['executionsTotal']);
$this->assertIsNumeric($response['body']['executionsTimeTotal']);
$this->assertIsNumeric($response['body']['executionsMbSecondsTotal']);
$this->assertIsArray($response['body']['deployments']);
$this->assertIsArray($response['body']['deploymentsStorage']);
$this->assertIsArray($response['body']['builds']);
$this->assertIsArray($response['body']['buildsTime']);
$this->assertIsArray($response['body']['buildsStorage']);
$this->assertIsArray($response['body']['buildsTime']);
$this->assertIsArray($response['body']['buildsMbSeconds']);
$this->assertIsArray($response['body']['executions']);
$this->assertIsArray($response['body']['executionsTime']);
$this->assertIsArray($response['body']['executionsMbSeconds']);
/**
* Test for FAILURE
*/
$usage = $this->getFunctionUsage($data['functionId'], [
'range' => '232h'
]);
$this->assertEquals(400, $usage['headers']['status-code']);
$usage = $this->getFunctionUsage('randomFunctionId', [
'range' => '24h'
]);
$this->assertEquals(404, $usage['headers']['status-code']);
}
/**
@ -123,31 +106,53 @@ class FunctionsConsoleClientTest extends Scope
/**
* Test for SUCCESS
*/
$variable = $this->createVariable(
$data['functionId'],
[
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE'
]
);
$response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'APP_TEST',
'value' => 'TESTINGVALUE'
]);
$this->assertEquals(201, $variable['headers']['status-code']);
$this->assertEquals(201, $response['headers']['status-code']);
$variableId = $response['body']['$id'];
$variableId = $variable['body']['$id'];
/**
* Test for FAILURE
*/
// Test for duplicate key
$variable = $this->createVariable(
$data['functionId'],
[
'key' => 'APP_TEST',
'value' => 'ANOTHERTESTINGVALUE'
]
);
$response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'APP_TEST',
'value' => 'ANOTHER_TESTINGVALUE'
]);
$this->assertEquals(409, $variable['headers']['status-code']);
$this->assertEquals(409, $response['headers']['status-code']);
// Test for invalid key
$variable = $this->createVariable(
$data['functionId'],
[
'key' => str_repeat("A", 256),
'value' => 'TESTINGVALUE'
]
);
$this->assertEquals(400, $variable['headers']['status-code']);
// Test for invalid value
$variable = $this->createVariable(
$data['functionId'],
[
'key' => 'LONGKEY',
'value' => str_repeat("#", 8193),
]
);
$this->assertEquals(400, $variable['headers']['status-code']);
return array_merge(
$data,
@ -155,28 +160,6 @@ class FunctionsConsoleClientTest extends Scope
'variableId' => $variableId
]
);
$longKey = str_repeat("A", 256);
$response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => $longKey,
'value' => 'TESTINGVALUE'
]);
$this->assertEquals(400, $response['headers']['status-code']);
$longValue = str_repeat("#", 8193);
$response = $this->client->call(Client::METHOD_POST, '/functions/' . $data['functionId'] . '/variables', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'key' => 'LONGKEY',
'value' => $longValue
]);
$this->assertEquals(400, $response['headers']['status-code']);
}
/**

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,214 @@
<?php
namespace Tests\E2E\Services\Functions;
use Appwrite\ID;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideServer;
use Utopia\Database\Helpers\Role;
class FunctionsScheduleTest extends Scope
{
use FunctionsBase;
use ProjectCustom;
use SideServer;
public function testCreateScheduledExecution()
{
/**
* Test for SUCCESS
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'events' => [
'users.*.create',
'users.*.delete',
],
'schedule' => '* * * * *', // Execute every 60 seconds
'timeout' => 10,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'activate' => true
]);
// Wait for scheduled execution
\sleep(60);
$this->assertEventually(function () use ($functionId) {
$executions = $this->client->call(Client::METHOD_GET, '/functions/' . $functionId . '/executions', [
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
]);
$this->assertEquals(200, $executions['headers']['status-code']);
$this->assertCount(1, $executions['body']['executions']);
$asyncExecution = $executions['body']['executions'][0];
$this->assertEquals('schedule', $asyncExecution['trigger']);
$this->assertEquals('completed', $asyncExecution['status']);
$this->assertEquals(200, $asyncExecution['responseStatusCode']);
$this->assertEquals('', $asyncExecution['responseBody']);
$this->assertNotEmpty($asyncExecution['logs']);
$this->assertNotEmpty($asyncExecution['errors']);
$this->assertGreaterThan(0, $asyncExecution['duration']);
}, 60000, 500);
$this->cleanupFunction($functionId);
}
public function testCreateScheduledAtExecution(): void
{
/**
* Test for SUCCESS
*/
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'activate' => true
]);
// Schedule execution for the future
\date_default_timezone_set('UTC');
$futureTime = (new \DateTime())->add(new \DateInterval('PT2M')); // 2 minute in the future
$futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0);
$execution = $this->client->call(
Client::METHOD_POST,
'/functions/' . $functionId . '/executions',
[
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'origin' => 'http://localhost',
'cookie' => 'a_session_' . $this->getProject()['$id'] . '=' . $this->getUser()['session'],
],
[
'async' => true,
'scheduledAt' => $futureTime->format(\DateTime::ATOM),
'path' => '/custom-path',
'method' => 'PATCH',
'body' => 'custom-body',
'headers' => [
'x-custom-header' => 'custom-value'
]
]
);
$executionId = $execution['body']['$id'];
$this->assertEquals(202, $execution['headers']['status-code']);
$this->assertEquals('scheduled', $execution['body']['status']);
$this->assertEquals('PATCH', $execution['body']['requestMethod']);
$this->assertEquals('/custom-path', $execution['body']['requestPath']);
$this->assertCount(0, $execution['body']['requestHeaders']);
\sleep(120);
$this->assertEventually(function () use ($functionId, $executionId) {
$execution = $this->getExecution($functionId, $executionId);
$this->assertEquals(200, $execution['headers']['status-code']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals('completed', $execution['body']['status']);
$this->assertEquals('/custom-path', $execution['body']['requestPath']);
$this->assertEquals('PATCH', $execution['body']['requestMethod']);
$this->assertStringContainsString('body-is-custom-body', $execution['body']['logs']);
$this->assertStringContainsString('custom-header-is-custom-value', $execution['body']['logs']);
$this->assertStringContainsString('method-is-patch', $execution['body']['logs']);
$this->assertStringContainsString('path-is-/custom-path', $execution['body']['logs']);
$this->assertStringContainsString('user-is-' . $this->getUser()['$id'], $execution['body']['logs']);
$this->assertStringContainsString('jwt-is-valid', $execution['body']['logs']);
$this->assertGreaterThan(0, $execution['body']['duration']);
}, 10000, 500);
/* Test for FAILURE */
// Schedule synchronous execution
$execution = $this->createExecution($functionId, [
'async' => 'false',
'scheduledAt' => $futureTime->format(\DateTime::ATOM),
]);
$this->assertEquals(400, $execution['headers']['status-code']);
// Execution with seconds precision
$execution = $this->createExecution($functionId, [
'async' => true,
'scheduledAt' => (new \DateTime("2100-12-08 16:12:02"))->format(\DateTime::ATOM)
]);
$this->assertEquals(400, $execution['headers']['status-code']);
// Execution with milliseconds precision
$execution = $this->createExecution($functionId, [
'async' => true,
'scheduledAt' => (new \DateTime("2100-12-08 16:12:02.255"))->format(\DateTime::ATOM)
]);
$this->assertEquals(400, $execution['headers']['status-code']);
// Execution too soon
$execution = $this->createExecution($functionId, [
'async' => true,
'scheduledAt' => (new \DateTime())->add(new \DateInterval('PT1S'))->format(\DateTime::ATOM)
]);
$this->assertEquals(400, $execution['headers']['status-code']);
$this->cleanupFunction($functionId, $executionId);
}
public function testDeleteScheduledExecution()
{
$functionId = $this->setupFunction([
'functionId' => ID::unique(),
'name' => 'Test',
'execute' => [Role::user($this->getUser()['$id'])->toString()],
'runtime' => 'php-8.0',
'entrypoint' => 'index.php',
'timeout' => 10,
'logging' => true,
]);
$this->setupDeployment($functionId, [
'entrypoint' => 'index.php',
'code' => $this->packageFunction('php'),
'activate' => true
]);
$futureTime = (new \DateTime())->add(new \DateInterval('PT10H'));
$futureTime->setTime($futureTime->format('H'), $futureTime->format('i'), 0, 0);
$execution = $this->createExecution($functionId, [
'async' => true,
'scheduledAt' => $futureTime->format('Y-m-d H:i:s'),
]);
$this->assertEquals(202, $execution['headers']['status-code']);
$executionId = $execution['body']['$id'] ?? '';
$execution = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId . '/executions/' . $executionId, array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $execution['headers']['status-code']);
$this->cleanupFunction($functionId);
}
}

View file

@ -2,6 +2,7 @@
namespace Tests\E2E\Services\GraphQL;
use CURLFile;
use Utopia\CLI\Console;
trait Base
@ -2496,8 +2497,17 @@ trait Base
protected string $stdout = '';
protected string $stderr = '';
protected function packageCode($folder): void
protected function packageFunction(string $function): CURLFile
{
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/$folder && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
$folderPath = realpath(__DIR__ . '/../../../resources/functions') . "/$function";
$tarPath = "$folderPath/code.tar.gz";
Console::execute("cd $folderPath && tar --exclude code.tar.gz -czf code.tar.gz .", '', $this->stdout, $this->stderr);
if (filesize($tarPath) > 1024 * 1024 * 5) {
throw new \Exception('Code package is too large. Use the chunked upload method instead.');
}
return new CURLFile($tarPath, 'application/x-gzip', \basename($tarPath));
}
}

View file

@ -2,7 +2,6 @@
namespace Tests\E2E\Services\GraphQL;
use CURLFile;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@ -83,10 +82,6 @@ class FunctionsClientTest extends Scope
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_DEPLOYMENT);
$folder = 'php';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
$this->packageCode($folder);
$gqlPayload = [
'operations' => \json_encode([
'query' => $query,
@ -99,7 +94,7 @@ class FunctionsClientTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => new CURLFile($code, 'application/gzip', 'code.tar.gz'),
'code' => $this->packageFunction('php')
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', [

View file

@ -2,7 +2,6 @@
namespace Tests\E2E\Services\GraphQL;
use CURLFile;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@ -82,10 +81,6 @@ class FunctionsServerTest extends Scope
$projectId = $this->getProject()['$id'];
$query = $this->getQuery(self::$CREATE_DEPLOYMENT);
$folder = 'php';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
$this->packageCode($folder);
$gqlPayload = [
'operations' => \json_encode([
'query' => $query,
@ -98,7 +93,7 @@ class FunctionsServerTest extends Scope
'map' => \json_encode([
'code' => ["variables.code"]
]),
'code' => new CURLFile($code, 'application/gzip', 'code.tar.gz'),
'code' => $this->packageFunction('php'),
];
$deployment = $this->client->call(Client::METHOD_POST, '/graphql', \array_merge([

View file

@ -2,7 +2,6 @@
namespace Tests\E2E\Services\Realtime;
use CURLFile;
use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
@ -506,21 +505,15 @@ class RealtimeConsoleClientTest extends Scope
* Test Create Deployment
*/
$folder = 'php';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/$folder/code.tar.gz";
$this->packageCode($folder);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'entrypoint' => 'index.php',
'code' => new CURLFile($code, 'application/x-gzip', \basename($code)),
'code' => $this->packageFunction('php'),
'activate' => true
]);
$deploymentId = $deployment['body']['$id'] ?? '';
$this->assertEquals(202, $deployment['headers']['status-code']);
$response = json_decode($client->receive(), true);

View file

@ -7,7 +7,7 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\CLI\Console;
use Tests\E2E\Services\Functions\FunctionsBase;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Helpers\Permission;
use Utopia\Database\Helpers\Role;
@ -15,6 +15,7 @@ use WebSocket\ConnectionException;
class RealtimeCustomClientTest extends Scope
{
use FunctionsBase;
use RealtimeBase;
use ProjectCustom;
use SideClient;
@ -1271,20 +1272,13 @@ class RealtimeCustomClientTest extends Scope
$this->assertEquals($function['headers']['status-code'], 201);
$this->assertNotEmpty($function['body']['$id']);
$folder = 'timeout';
$stderr = '';
$stdout = '';
$code = realpath(__DIR__ . '/../../../resources/functions') . "/{$folder}/code.tar.gz";
Console::execute('cd ' . realpath(__DIR__ . "/../../../resources/functions") . "/{$folder} && tar --exclude code.tar.gz -czf code.tar.gz .", '', $stdout, $stderr);
$deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey']
]), [
'entrypoint' => 'index.php',
'code' => new CURLFile($code, 'application/x-gzip', basename($code)),
'code' => $this->packageFunction('timeout'),
'activate' => true
]);

View file

@ -0,0 +1,17 @@
<?php
namespace Appwrite\Tests;
use Appwrite\Tests\Async\Eventually;
use PHPUnit\Framework\Assert;
const DEFAULT_TIMEOUT_MS = 10000;
const DEFAULT_WAIT_MS = 500;
trait Async
{
public static function assertEventually(callable $probe, int $timeoutMs = DEFAULT_TIMEOUT_MS, int $waitMs = DEFAULT_WAIT_MS): void
{
Assert::assertThat($probe, new Eventually($timeoutMs, $waitMs));
}
}

View file

@ -0,0 +1,49 @@
<?php
namespace Appwrite\Tests\Async;
use PHPUnit\Framework\Constraint\Constraint;
final class Eventually extends Constraint
{
public function __construct(private int $timeoutMs, private int $waitMs)
{
}
public function evaluate(mixed $probe, string $description = '', bool $returnResult = false): ?bool
{
if (!is_callable($probe)) {
throw new \Exception('Probe must be a callable');
}
$start = microtime(true);
$lastException = null;
do {
try {
$probe();
return true;
} catch (\Exception $exception) {
$lastException = $exception;
}
usleep($this->waitMs * 1000);
} while (microtime(true) - $start < $this->timeoutMs / 1000);
if ($returnResult) {
return false;
}
throw $lastException;
}
protected function failureDescription(mixed $other): string
{
return 'the given probe was satisfied within ' . $this->timeoutMs . 'ms.';
}
public function toString(): string
{
return 'Eventually';
}
}