[Platform] Split bridges into dedicated packages

This commit is contained in:
Oskar Stark
2025-12-09 21:32:18 +01:00
parent ee68d9f6e5
commit 7f4ca0fb08
11 changed files with 689 additions and 0 deletions

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
Scaleway Platform
=================
Scaleway platform bridge for Symfony AI.
Resources
---------
* [Contributing](https://symfony.com/doc/current/contributing/index.html)
* [Report issues](https://github.com/symfony/ai/issues) and
[send Pull Requests](https://github.com/symfony/ai/pulls)
in the [main Symfony AI repository](https://github.com/symfony/ai)

View File

@@ -0,0 +1,93 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway\Embeddings;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\Embeddings;
use Symfony\AI\Platform\Bridge\Scaleway\Embeddings\ModelClient;
use Symfony\AI\Platform\Exception\InvalidArgumentException;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class ModelClientTest extends TestCase
{
public function testItThrowsExceptionWhenApiKeyIsEmpty()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('The API key must not be empty.');
new ModelClient(new MockHttpClient(), '');
}
public function testItAcceptsValidApiKey()
{
$modelClient = new ModelClient(new MockHttpClient(), 'scaleway-valid-api-key');
$this->assertInstanceOf(ModelClient::class, $modelClient);
}
public function testItIsSupportingTheCorrectModel()
{
$modelClient = new ModelClient(new MockHttpClient(), 'scaleway-api-key');
$this->assertTrue($modelClient->supports(new Embeddings('bge-multilingual-gemma2')));
}
public function testItIsExecutingTheCorrectRequest()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/embeddings', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
self::assertSame('{"model":"bge-multilingual-gemma2","input":"test text"}', $options['body']);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Embeddings('bge-multilingual-gemma2'), 'test text', []);
}
public function testItIsExecutingTheCorrectRequestWithCustomOptions()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/embeddings', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
self::assertSame('{"dimensions":256,"model":"bge-multilingual-gemma2","input":"test text"}', $options['body']);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Embeddings('bge-multilingual-gemma2'), 'test text', ['dimensions' => 256]);
}
public function testItIsExecutingTheCorrectRequestWithArrayInput()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/embeddings', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
self::assertSame('{"model":"bge-multilingual-gemma2","input":["text1","text2","text3"]}', $options['body']);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Embeddings('bge-multilingual-gemma2'), ['text1', 'text2', 'text3'], []);
}
}

View File

@@ -0,0 +1,60 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway\Embeddings;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\Embeddings\ResultConverter;
use Symfony\AI\Platform\Result\RawHttpResult;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class ResultConverterTest extends TestCase
{
public function testItConvertsAResponseToAVectorResult()
{
$result = $this->createStub(ResponseInterface::class);
$result
->method('toArray')
->willReturn(json_decode($this->getEmbeddingStub(), true));
$vectorResult = (new ResultConverter())->convert(new RawHttpResult($result));
$convertedContent = $vectorResult->getContent();
$this->assertCount(2, $convertedContent);
$this->assertSame([0.3, 0.4, 0.4], $convertedContent[0]->getData());
$this->assertSame([0.0, 0.0, 0.2], $convertedContent[1]->getData());
}
private function getEmbeddingStub(): string
{
return <<<'JSON'
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.3, 0.4, 0.4]
},
{
"object": "embedding",
"index": 1,
"embedding": [0.0, 0.0, 0.2]
}
]
}
JSON;
}
}

29
Tests/EmbeddingsTest.php Normal file
View File

@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\Embeddings;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class EmbeddingsTest extends TestCase
{
public function testItCreatesEmbeddingsWithDefaultSettings()
{
$embeddings = new Embeddings('bge-multilingual-gemma2');
$this->assertSame('bge-multilingual-gemma2', $embeddings->getName());
$this->assertSame([], $embeddings->getOptions());
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway\Llm;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\Llm\ModelClient;
use Symfony\AI\Platform\Bridge\Scaleway\Scaleway;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;
use Symfony\Contracts\HttpClient\ResponseInterface as HttpResponse;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class ModelClientTest extends TestCase
{
public function testItAcceptsValidApiKey()
{
$modelClient = new ModelClient(new MockHttpClient(), 'scaleway-valid-api-key');
$this->assertInstanceOf(ModelClient::class, $modelClient);
}
public function testItWrapsHttpClientInEventSourceHttpClient()
{
$httpClient = new MockHttpClient();
$modelClient = new ModelClient($httpClient, 'scaleway-valid-api-key');
$this->assertInstanceOf(ModelClient::class, $modelClient);
}
public function testItAcceptsEventSourceHttpClientDirectly()
{
$httpClient = new EventSourceHttpClient(new MockHttpClient());
$modelClient = new ModelClient($httpClient, 'scaleway-valid-api-key');
$this->assertInstanceOf(ModelClient::class, $modelClient);
}
public function testItIsSupportingTheCorrectModel()
{
$modelClient = new ModelClient(new MockHttpClient(), 'sk-api-key');
$this->assertTrue($modelClient->supports(new Scaleway('deepseek-r1-distill-llama-70b')));
}
public function testItIsExecutingTheCorrectRequest()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/chat/completions', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
self::assertSame('{"temperature":1,"model":"deepseek-r1-distill-llama-70b","messages":[{"role":"user","content":"test message"}]}', $options['body']);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Scaleway('deepseek-r1-distill-llama-70b'), ['model' => 'deepseek-r1-distill-llama-70b', 'messages' => [['role' => 'user', 'content' => 'test message']]], ['temperature' => 1]);
}
public function testItIsExecutingTheCorrectRequestWithArrayPayload()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/chat/completions', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
self::assertSame('{"temperature":0.7,"model":"deepseek-r1-distill-llama-70b","messages":[{"role":"user","content":"Hello"}]}', $options['body']);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Scaleway('deepseek-r1-distill-llama-70b'), ['model' => 'deepseek-r1-distill-llama-70b', 'messages' => [['role' => 'user', 'content' => 'Hello']]], ['temperature' => 0.7]);
}
public function testItUsesCorrectBaseUrl()
{
$resultCallback = static function (string $method, string $url, array $options): HttpResponse {
self::assertSame('POST', $method);
self::assertSame('https://api.scaleway.ai/v1/chat/completions', $url);
self::assertSame('Authorization: Bearer scaleway-api-key', $options['normalized_headers']['authorization'][0]);
return new MockResponse();
};
$httpClient = new MockHttpClient([$resultCallback]);
$modelClient = new ModelClient($httpClient, 'scaleway-api-key');
$modelClient->request(new Scaleway('deepseek-r1-distill-llama-70b'), ['messages' => []], []);
}
}

View File

@@ -0,0 +1,184 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway\Llm;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\Llm\ResultConverter;
use Symfony\AI\Platform\Exception\ContentFilterException;
use Symfony\AI\Platform\Exception\RuntimeException;
use Symfony\AI\Platform\Result\ChoiceResult;
use Symfony\AI\Platform\Result\RawHttpResult;
use Symfony\AI\Platform\Result\TextResult;
use Symfony\AI\Platform\Result\ToolCallResult;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class ResultConverterTest extends TestCase
{
public function testConvertTextResult()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->method('toArray')->willReturn([
'choices' => [
[
'message' => [
'role' => 'assistant',
'content' => 'Hello world',
],
'finish_reason' => 'stop',
],
],
]);
$result = $converter->convert(new RawHttpResult($httpResponse));
$this->assertInstanceOf(TextResult::class, $result);
$this->assertSame('Hello world', $result->getContent());
}
public function testConvertToolCallResult()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->method('toArray')->willReturn([
'choices' => [
[
'message' => [
'role' => 'assistant',
'content' => null,
'tool_calls' => [
[
'id' => 'call_123',
'type' => 'function',
'function' => [
'name' => 'test_function',
'arguments' => '{"arg1": "value1"}',
],
],
],
],
'finish_reason' => 'tool_calls',
],
],
]);
$result = $converter->convert(new RawHttpResult($httpResponse));
$this->assertInstanceOf(ToolCallResult::class, $result);
$toolCalls = $result->getContent();
$this->assertCount(1, $toolCalls);
$this->assertSame('call_123', $toolCalls[0]->getId());
$this->assertSame('test_function', $toolCalls[0]->getName());
$this->assertSame(['arg1' => 'value1'], $toolCalls[0]->getArguments());
}
public function testConvertMultipleChoices()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->method('toArray')->willReturn([
'choices' => [
[
'message' => [
'role' => 'assistant',
'content' => 'Choice 1',
],
'finish_reason' => 'stop',
],
[
'message' => [
'role' => 'assistant',
'content' => 'Choice 2',
],
'finish_reason' => 'stop',
],
],
]);
$result = $converter->convert(new RawHttpResult($httpResponse));
$this->assertInstanceOf(ChoiceResult::class, $result);
$choices = $result->getContent();
$this->assertCount(2, $choices);
$this->assertSame('Choice 1', $choices[0]->getContent());
$this->assertSame('Choice 2', $choices[1]->getContent());
}
public function testContentFilterException()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->expects($this->exactly(1))
->method('toArray')
->willReturnCallback(function ($throw = true) {
if ($throw) {
throw new class extends \Exception implements ClientExceptionInterface {
public function getResponse(): ResponseInterface
{
throw new RuntimeException('Not implemented');
}
};
}
return [
'error' => [
'code' => 'content_filter',
'message' => 'Content was filtered',
],
];
});
$this->expectException(ContentFilterException::class);
$this->expectExceptionMessage('Content was filtered');
$converter->convert(new RawHttpResult($httpResponse));
}
public function testThrowsExceptionWhenNoChoices()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->method('toArray')->willReturn([]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Result does not contain choices');
$converter->convert(new RawHttpResult($httpResponse));
}
public function testThrowsExceptionForUnsupportedFinishReason()
{
$converter = new ResultConverter();
$httpResponse = self::createMock(ResponseInterface::class);
$httpResponse->method('toArray')->willReturn([
'choices' => [
[
'message' => [
'role' => 'assistant',
'content' => 'Test content',
],
'finish_reason' => 'unsupported_reason',
],
],
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Unsupported finish reason "unsupported_reason"');
$converter->convert(new RawHttpResult($httpResponse));
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\AI\Platform\Bridge\Scaleway\Tests;
use Symfony\AI\Platform\Bridge\Scaleway\Embeddings;
use Symfony\AI\Platform\Bridge\Scaleway\ModelCatalog;
use Symfony\AI\Platform\Bridge\Scaleway\Scaleway;
use Symfony\AI\Platform\Capability;
use Symfony\AI\Platform\ModelCatalog\ModelCatalogInterface;
use Symfony\AI\Platform\Test\ModelCatalogTestCase;
/**
* @author Oskar Stark <oskarstark@googlemail.com>
*/
final class ModelCatalogTest extends ModelCatalogTestCase
{
public static function modelsProvider(): iterable
{
// Scaleway models
yield 'deepseek-r1-distill-llama-70b' => ['deepseek-r1-distill-llama-70b', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'gemma-3-27b-it' => ['gemma-3-27b-it', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'llama-3.1-8b-instruct' => ['llama-3.1-8b-instruct', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'llama-3.3-70b-instruct' => ['llama-3.3-70b-instruct', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'devstral-small-2505' => ['devstral-small-2505', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'mistral-nemo-instruct-2407' => ['mistral-nemo-instruct-2407', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'pixtral-12b-2409' => ['pixtral-12b-2409', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::INPUT_IMAGE, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'mistral-small-3.2-24b-instruct-2506' => ['mistral-small-3.2-24b-instruct-2506', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'gpt-oss-120b' => ['gpt-oss-120b', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'qwen3-coder-30b-a3b-instruct' => ['qwen3-coder-30b-a3b-instruct', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
yield 'qwen3-235b-a22b-instruct-2507' => ['qwen3-235b-a22b-instruct-2507', Scaleway::class, [Capability::INPUT_MESSAGES, Capability::OUTPUT_TEXT, Capability::OUTPUT_STREAMING, Capability::TOOL_CALLING, Capability::OUTPUT_STRUCTURED]];
// Embedding models
yield 'bge-multilingual-gemma2' => ['bge-multilingual-gemma2', Embeddings::class, [Capability::INPUT_TEXT]];
}
protected function createModelCatalog(): ModelCatalogInterface
{
return new ModelCatalog();
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Bridge\Scaleway;
use PHPUnit\Framework\TestCase;
use Symfony\AI\Platform\Bridge\Scaleway\PlatformFactory;
use Symfony\AI\Platform\Platform;
use Symfony\Component\HttpClient\EventSourceHttpClient;
use Symfony\Component\HttpClient\MockHttpClient;
/**
* @author Marcus Stöhr <marcus@fischteich.net>
*/
final class PlatformFactoryTest extends TestCase
{
public function testItCreatesPlatformWithDefaultSettings()
{
$platform = PlatformFactory::create('scaleway-test-api-key');
$this->assertInstanceOf(Platform::class, $platform);
}
public function testItCreatesPlatformWithCustomHttpClient()
{
$httpClient = new MockHttpClient();
$platform = PlatformFactory::create('scaleway-test-api-key', $httpClient);
$this->assertInstanceOf(Platform::class, $platform);
}
public function testItCreatesPlatformWithEventSourceHttpClient()
{
$httpClient = new EventSourceHttpClient(new MockHttpClient());
$platform = PlatformFactory::create('scaleway-test-api-key', $httpClient);
$this->assertInstanceOf(Platform::class, $platform);
}
}

60
composer.json Normal file
View File

@@ -0,0 +1,60 @@
{
"name": "symfony/ai-scaleway-platform",
"description": "Scaleway platform bridge for Symfony AI",
"license": "MIT",
"type": "symfony-ai-platform",
"keywords": [
"ai",
"bridge",
"platform",
"scaleway"
],
"authors": [
{
"name": "Christopher Hertel",
"email": "mail@christopher-hertel.de"
},
{
"name": "Oskar Stark",
"email": "oskarstark@googlemail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"require": {
"php": ">=8.2",
"symfony/ai-platform": "@dev",
"symfony/http-client": "^7.3|^8.0"
},
"require-dev": {
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-strict-rules": "^2.0",
"phpunit/phpunit": "^11.5.46"
},
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"Symfony\\AI\\Platform\\Bridge\\Scaleway\\": ""
}
},
"autoload-dev": {
"psr-4": {
"Symfony\\AI\\PHPStan\\": "../../../../../.phpstan/",
"Symfony\\AI\\Platform\\Bridge\\Scaleway\\Tests\\": "Tests/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-main": "0.x-dev"
},
"thanks": {
"name": "symfony/ai",
"url": "https://github.com/symfony/ai"
}
}
}

23
phpstan.dist.neon Normal file
View File

@@ -0,0 +1,23 @@
includes:
- ../../../../../.phpstan/extension.neon
parameters:
level: 6
paths:
- .
- Tests/
excludePaths:
- vendor/
treatPhpDocTypesAsCertain: false
ignoreErrors:
-
message: "#^Method .*::test.*\\(\\) has no return type specified\\.$#"
reportUnmatched: false
-
identifier: missingType.iterableValue
path: Tests/*
reportUnmatched: false
-
identifier: 'symfonyAi.forbidNativeException'
path: Tests/*
reportUnmatched: false

32
phpunit.xml.dist Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
failOnDeprecation="true"
failOnRisky="true"
failOnWarning="true"
>
<php>
<ini name="error_reporting" value="-1" />
</php>
<testsuites>
<testsuite name="Symfony AI Scaleway Platform Test Suite">
<directory>./Tests/</directory>
</testsuite>
</testsuites>
<source ignoreSuppressionOfDeprecations="true">
<include>
<directory>./</directory>
</include>
<exclude>
<directory>./Resources</directory>
<directory>./Tests</directory>
<directory>./vendor</directory>
</exclude>
</source>
</phpunit>