Compare commits

..

4 Commits
v2.2.0 ... 2.1

Author SHA1 Message Date
Fabien Potencier
0e9d7c1215 fixed CS 2013-05-06 12:48:41 +02:00
Tim Nagel
05639e1018 Fixed test to use Reflection 2013-03-08 15:45:32 +11:00
Kris Wallsmith
4288c63972 [ClassLoader] tweaked test 2013-03-03 08:31:27 -08:00
Kris Wallsmith
bdb7ba2680 [ClassLoader] made DebugClassLoader idempotent 2013-03-02 11:24:53 -08:00
18 changed files with 176 additions and 351 deletions

2
.gitignore vendored
View File

@@ -1,4 +1,2 @@
vendor/
composer.lock
phpunit.xml

View File

@@ -42,38 +42,28 @@ namespace Symfony\Component\ClassLoader;
class ApcClassLoader
{
private $prefix;
/**
* The class loader object being decorated.
*
* @var \Symfony\Component\ClassLoader\ClassLoader
* A class loader object that implements the findFile() method.
*/
protected $decorated;
private $classFinder;
/**
* Constructor.
*
* @param string $prefix The APC namespace prefix to use.
* @param object $decorated A class loader object that implements the findFile() method.
*
* @throws \RuntimeException
* @throws \InvalidArgumentException
* @param string $prefix A prefix to create a namespace in APC
* @param object $classFinder An object that implements findFile() method.
*
* @api
*/
public function __construct($prefix, $decorated)
public function __construct($prefix, $classFinder)
{
if (!extension_loaded('apc')) {
throw new \RuntimeException('Unable to use ApcClassLoader as APC is not enabled.');
}
if (!method_exists($decorated, 'findFile')) {
if (!method_exists($classFinder, 'findFile')) {
throw new \InvalidArgumentException('The class finder must implement a "findFile" method.');
}
$this->prefix = $prefix;
$this->decorated = $decorated;
$this->classFinder = $classFinder;
}
/**
@@ -120,18 +110,9 @@ class ApcClassLoader
public function findFile($class)
{
if (false === $file = apc_fetch($this->prefix.$class)) {
apc_store($this->prefix.$class, $file = $this->decorated->findFile($class));
apc_store($this->prefix.$class, $file = $this->classFinder->findFile($class));
}
return $file;
}
/**
* Passes through all unknown calls onto the decorated object.
*/
public function __call($method, $args)
{
return call_user_func_array(array($this->decorated, $method), $args);
}
}

View File

@@ -69,8 +69,6 @@ class ApcUniversalClassLoader extends UniversalClassLoader
*
* @param string $prefix A prefix to create a namespace in APC
*
* @throws \RuntimeException
*
* @api
*/
public function __construct($prefix)

View File

@@ -104,14 +104,14 @@ class ClassCollectionLoader
$c = preg_replace(array('/^\s*<\?php/', '/\?>\s*$/'), '', file_get_contents($class->getFileName()));
// fakes namespace declaration for global code
// add namespace declaration for global code
if (!$class->inNamespace()) {
$c = "\nnamespace\n{\n".$c."\n}\n";
$c = "\nnamespace\n{\n".self::stripComments($c)."\n}\n";
} else {
$c = self::fixNamespaceDeclarations('<?php '.$c);
$c = preg_replace('/^\s*<\?php/', '', $c);
}
$c = self::fixNamespaceDeclarations('<?php '.$c);
$c = preg_replace('/^\s*<\?php/', '', $c);
$content .= $c;
}
@@ -144,78 +144,45 @@ class ClassCollectionLoader
return $source;
}
$rawChunk = '';
$output = '';
$inNamespace = false;
$tokens = token_get_all($source);
for (reset($tokens); false !== $token = current($tokens); next($tokens)) {
for ($i = 0, $max = count($tokens); $i < $max; $i++) {
$token = $tokens[$i];
if (is_string($token)) {
$rawChunk .= $token;
$output .= $token;
} elseif (in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
// strip comments
continue;
} elseif (T_NAMESPACE === $token[0]) {
if ($inNamespace) {
$rawChunk .= "}\n";
$output .= "}\n";
}
$rawChunk .= $token[1];
$output .= $token[1];
// namespace name and whitespaces
while (($t = next($tokens)) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
$rawChunk .= $t[1];
while (($t = $tokens[++$i]) && is_array($t) && in_array($t[0], array(T_WHITESPACE, T_NS_SEPARATOR, T_STRING))) {
$output .= $t[1];
}
if ('{' === $t) {
if (is_string($t) && '{' === $t) {
$inNamespace = false;
prev($tokens);
--$i;
} else {
$rawChunk = rtrim($rawChunk) . "\n{";
$output = rtrim($output);
$output .= "\n{";
$inNamespace = true;
}
} elseif (T_START_HEREDOC === $token[0]) {
$output .= self::compressCode($rawChunk) . $token[1];
do {
$token = next($tokens);
$output .= $token[1];
} while ($token[0] !== T_END_HEREDOC);
$rawChunk = '';
} elseif (T_CONSTANT_ENCAPSED_STRING === $token[0]) {
$output .= self::compressCode($rawChunk) . $token[1];
$rawChunk = '';
} else {
$rawChunk .= $token[1];
$output .= $token[1];
}
}
if ($inNamespace) {
$rawChunk .= "}\n";
$output .= "}\n";
}
return $output . self::compressCode($rawChunk);
}
/**
* This method is only useful for testing.
*/
public static function enableTokenizer($bool)
{
self::$useTokenizer = (Boolean) $bool;
}
/**
* Strips leading & trailing ws, multiple EOL, multiple ws.
*
* @param string $code Original PHP code
*
* @return string compressed code
*/
private static function compressCode($code)
{
return preg_replace(
array('/^\s+/m', '/\s+$/m', '/([\n\r]+ *[\n\r]+)+/', '/[ \t]+/'),
array('', '', "\n", ' '),
$code
);
return $output;
}
/**
@@ -238,12 +205,43 @@ class ClassCollectionLoader
throw new \RuntimeException(sprintf('Failed to write cache file "%s".', $file));
}
/**
* Removes comments from a PHP source string.
*
* We don't use the PHP php_strip_whitespace() function
* as we want the content to be readable and well-formatted.
*
* @param string $source A PHP string
*
* @return string The PHP string with the comments removed
*/
private static function stripComments($source)
{
if (!function_exists('token_get_all')) {
return $source;
}
$output = '';
foreach (token_get_all($source) as $token) {
if (is_string($token)) {
$output .= $token;
} elseif (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT))) {
$output .= $token[1];
}
}
// replace multiple new lines with a single newline
$output = preg_replace(array('/\s+$/Sm', '/\n+/S'), "\n", $output);
return $output;
}
/**
* Gets an ordered array of passed classes including all their dependencies.
*
* @param array $classes
*
* @return \ReflectionClass[] An array of sorted \ReflectionClass instances (dependencies added if needed)
* @return array An array of sorted \ReflectionClass instances (dependencies added if needed)
*
* @throws \InvalidArgumentException When a class can't be loaded
*/
@@ -280,19 +278,17 @@ class ClassCollectionLoader
array_unshift($classes, $parent);
}
$traits = array();
if (function_exists('get_declared_traits')) {
foreach ($classes as $c) {
foreach (self::resolveDependencies(self::computeTraitDeps($c), $c) as $trait) {
if ($trait !== $c) {
$traits[] = $trait;
}
foreach (self::getTraits($c) as $trait) {
self::$seen[$trait->getName()] = true;
array_unshift($classes, $trait);
}
}
}
return array_merge(self::getInterfaces($class), $traits, $classes);
return array_merge(self::getInterfaces($class), $classes);
}
private static function getInterfaces(\ReflectionClass $class)
@@ -312,54 +308,26 @@ class ClassCollectionLoader
return $classes;
}
private static function computeTraitDeps(\ReflectionClass $class)
private static function getTraits(\ReflectionClass $class)
{
$traits = $class->getTraits();
$deps = array($class->getName() => $traits);
$classes = array();
while ($trait = array_pop($traits)) {
if ($trait->isUserDefined() && !isset(self::$seen[$trait->getName()])) {
self::$seen[$trait->getName()] = true;
$traitDeps = $trait->getTraits();
$deps[$trait->getName()] = $traitDeps;
$traits = array_merge($traits, $traitDeps);
$classes[] = $trait;
$traits = array_merge($traits, $trait->getTraits());
}
}
return $deps;
return $classes;
}
/**
* Dependencies resolution.
*
* This function does not check for circular dependencies as it should never
* occur with PHP traits.
*
* @param array $tree The dependency tree
* @param \ReflectionClass $node The node
* @param \ArrayObject $resolved An array of already resolved dependencies
* @param \ArrayObject $unresolved An array of dependencies to be resolved
*
* @return \ArrayObject The dependencies for the given node
*
* @throws \RuntimeException if a circular dependency is detected
* This method is only useful for testing.
*/
private static function resolveDependencies(array $tree, $node, \ArrayObject $resolved = null, \ArrayObject $unresolved = null)
public static function enableTokenizer($bool)
{
if (null === $resolved) {
$resolved = new \ArrayObject();
}
if (null === $unresolved) {
$unresolved = new \ArrayObject();
}
$nodeName = $node->getName();
$unresolved[$nodeName] = $node;
foreach ($tree[$nodeName] as $dependency) {
if (!$resolved->offsetExists($dependency->getName())) {
self::resolveDependencies($tree, $dependency, $resolved, $unresolved);
}
}
$resolved[$nodeName] = $node;
unset($unresolved[$nodeName]);
return $resolved;
self::$useTokenizer = (Boolean) $bool;
}
}

View File

@@ -53,7 +53,7 @@ class DebugClassLoader
}
foreach ($functions as $function) {
if (is_array($function) && method_exists($function[0], 'findFile')) {
if (is_array($function) && !$function[0] instanceof self && method_exists($function[0], 'findFile')) {
$function = array(new static($function[0]), 'loadClass');
}
@@ -87,8 +87,6 @@ class DebugClassLoader
* @param string $class The name of the class
*
* @return Boolean|null True, if loaded
*
* @throws \RuntimeException
*/
public function loadClass($class)
{

View File

@@ -64,6 +64,9 @@ Resources
You can run the unit tests with the following command:
$ cd path/to/Symfony/Component/ClassLoader/
$ composer.phar install --dev
$ phpunit
phpunit
If you also want to run the unit tests that depend on other Symfony
Components, install dev dependencies before running PHPUnit:
php composer.phar install --dev

View File

@@ -20,35 +20,6 @@ require_once __DIR__.'/Fixtures/ClassesWithParents/A.php';
class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
{
public function testTraitDependencies()
{
if (version_compare(phpversion(), '5.4', '<')) {
$this->markTestSkipped('Requires PHP > 5.4');
return;
}
require_once __DIR__.'/Fixtures/deps/traits.php';
$r = new \ReflectionClass('Symfony\Component\ClassLoader\ClassCollectionLoader');
$m = $r->getMethod('getOrderedClasses');
$m->setAccessible(true);
$ordered = $m->invoke('Symfony\Component\ClassLoader\ClassCollectionLoader', array('CTFoo'));
$this->assertEquals(
array('TD', 'TC', 'TB', 'TA', 'TZ', 'CTFoo'),
array_map(function ($class) { return $class->getName(); }, $ordered)
);
$ordered = $m->invoke('Symfony\Component\ClassLoader\ClassCollectionLoader', array('CTBar'));
$this->assertEquals(
array('TD', 'TZ', 'TC', 'TB', 'TA', 'CTBar'),
array_map(function ($class) { return $class->getName(); }, $ordered)
);
}
/**
* @dataProvider getDifferentOrders
*/
@@ -100,8 +71,8 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
*/
public function testClassWithTraitsReordering(array $classes)
{
if (version_compare(phpversion(), '5.4', '<')) {
$this->markTestSkipped('Requires PHP > 5.4');
if (version_compare(phpversion(), '5.4.0', '<')) {
$this->markTestSkipped('Requires PHP > 5.4.0.');
return;
}
@@ -115,9 +86,9 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
$expected = array(
'ClassesWithParents\\GInterface',
'ClassesWithParents\\CInterface',
'ClassesWithParents\\CTrait',
'ClassesWithParents\\ATrait',
'ClassesWithParents\\BTrait',
'ClassesWithParents\\CTrait',
'ClassesWithParents\\B',
'ClassesWithParents\\A',
'ClassesWithParents\\D',
@@ -154,20 +125,8 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('<?php '.$expected, ClassCollectionLoader::fixNamespaceDeclarations('<?php '.$source));
}
public function getFixNamespaceDeclarationsData()
{
return array(
array("namespace;\nclass Foo {}\n", "namespace\n{\nclass Foo {}\n}"),
array("namespace Foo;\nclass Foo {}\n", "namespace Foo\n{\nclass Foo {}\n}"),
array("namespace Bar ;\nclass Foo {}\n", "namespace Bar\n{\nclass Foo {}\n}"),
array("namespace Foo\Bar;\nclass Foo {}\n", "namespace Foo\Bar\n{\nclass Foo {}\n}"),
array("namespace Foo\Bar\Bar\n{\nclass Foo {}\n}\n", "namespace Foo\Bar\Bar\n{\nclass Foo {}\n}"),
array("namespace\n{\nclass Foo {}\n}\n", "namespace\n{\nclass Foo {}\n}"),
);
}
/**
* @dataProvider getFixNamespaceDeclarationsDataWithoutTokenizer
* @dataProvider getFixNamespaceDeclarationsData
*/
public function testFixNamespaceDeclarationsWithoutTokenizer($source, $expected)
{
@@ -176,7 +135,7 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
ClassCollectionLoader::enableTokenizer(true);
}
public function getFixNamespaceDeclarationsDataWithoutTokenizer()
public function getFixNamespaceDeclarationsData()
{
return array(
array("namespace;\nclass Foo {}\n", "namespace\n{\nclass Foo {}\n}\n"),
@@ -193,64 +152,12 @@ class ClassCollectionLoaderTest extends \PHPUnit_Framework_TestCase
*/
public function testUnableToLoadClassException()
{
if (is_file($file = sys_get_temp_dir().'/foo.php')) {
unlink($file);
}
ClassCollectionLoader::load(array('SomeNotExistingClass'), sys_get_temp_dir(), 'foo', false);
ClassCollectionLoader::load(array('SomeNotExistingClass'), '', 'foo', false);
}
public function testCommentStripping()
public function testLoadTwiceClass()
{
if (is_file($file = sys_get_temp_dir().'/bar.php')) {
unlink($file);
}
spl_autoload_register($r = function ($class) {
require_once __DIR__.'/Fixtures/'.str_replace(array('\\', '_'), '/', $class).'.php';
});
ClassCollectionLoader::load(
array('Namespaced\\WithComments', 'Pearlike_WithComments'),
sys_get_temp_dir(),
'bar',
false
);
spl_autoload_unregister($r);
$this->assertEquals(<<<EOF
namespace Namespaced
{
class WithComments
{
public static \$loaded = true;
}
\$string ='string shoult not be modified';
\$heredoc =<<<HD
Heredoc should not be modified
HD;
\$nowdoc =<<<'ND'
Nowdoc should not be modified
ND;
}
namespace
{
class Pearlike_WithComments
{
public static \$loaded = true;
}
}
EOF
, str_replace("<?php \n", '', file_get_contents($file)));
unlink($file);
ClassCollectionLoader::load(array('Foo'), '', 'foo', false);
ClassCollectionLoader::load(array('Foo'), '', 'foo', false);
}
}

View File

@@ -72,10 +72,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
{
$data = array(
array(__DIR__.'/Fixtures/Namespaced', array(
'Namespaced\\Bar' => realpath(__DIR__).'/Fixtures/Namespaced/Bar.php',
'Namespaced\\Foo' => realpath(__DIR__).'/Fixtures/Namespaced/Foo.php',
'Namespaced\\Baz' => realpath(__DIR__).'/Fixtures/Namespaced/Baz.php',
'Namespaced\\WithComments' => realpath(__DIR__).'/Fixtures/Namespaced/WithComments.php',
'Namespaced\\Bar' => realpath(__DIR__).'/Fixtures/Namespaced/Bar.php',
'Namespaced\\Foo' => realpath(__DIR__).'/Fixtures/Namespaced/Foo.php',
'Namespaced\\Baz' => realpath(__DIR__).'/Fixtures/Namespaced/Baz.php',
)
),
array(__DIR__.'/Fixtures/beta/NamespaceCollision', array(
@@ -85,10 +84,9 @@ class ClassMapGeneratorTest extends \PHPUnit_Framework_TestCase
'NamespaceCollision\\C\\B\\Foo' => realpath(__DIR__).'/Fixtures/beta/NamespaceCollision/C/B/Foo.php',
)),
array(__DIR__.'/Fixtures/Pearlike', array(
'Pearlike_Foo' => realpath(__DIR__).'/Fixtures/Pearlike/Foo.php',
'Pearlike_Bar' => realpath(__DIR__).'/Fixtures/Pearlike/Bar.php',
'Pearlike_Baz' => realpath(__DIR__).'/Fixtures/Pearlike/Baz.php',
'Pearlike_WithComments' => realpath(__DIR__).'/Fixtures/Pearlike/WithComments.php',
'Pearlike_Foo' => realpath(__DIR__).'/Fixtures/Pearlike/Foo.php',
'Pearlike_Bar' => realpath(__DIR__).'/Fixtures/Pearlike/Bar.php',
'Pearlike_Baz' => realpath(__DIR__).'/Fixtures/Pearlike/Baz.php',
)),
array(__DIR__.'/Fixtures/classmap', array(
'Foo\\Bar\\A' => realpath(__DIR__).'/Fixtures/classmap/sameNsMultipleClasses.php',

View File

@@ -0,0 +1,52 @@
<?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\Component\ClassLoader\Tests;
use Symfony\Component\ClassLoader\ClassLoader;
use Symfony\Component\ClassLoader\DebugClassLoader;
class DebugClassLoaderTest extends \PHPUnit_Framework_TestCase
{
private $loader;
protected function setUp()
{
$this->loader = new ClassLoader();
spl_autoload_register(array($this->loader, 'loadClass'));
}
protected function tearDown()
{
spl_autoload_unregister(array($this->loader, 'loadClass'));
}
public function testIdempotence()
{
DebugClassLoader::enable();
DebugClassLoader::enable();
$functions = spl_autoload_functions();
foreach ($functions as $function) {
if (is_array($function) && $function[0] instanceof DebugClassLoader) {
$reflClass = new \ReflectionClass($function[0]);
$reflProp = $reflClass->getProperty('classFinder');
$reflProp->setAccessible(true);
$this->assertNotInstanceOf('Symfony\Component\ClassLoader\DebugClassLoader', $reflProp->getValue($function[0]));
return;
}
}
throw new \Exception('DebugClassLoader did not register');
}
}

View File

@@ -1,37 +0,0 @@
<?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 Namespaced;
class WithComments
{
/** @Boolean */
public static $loaded = true;
}
$string = 'string shoult not be modified';
$heredoc = <<<HD
Heredoc should not be modified
HD;
$nowdoc = <<<'ND'
Nowdoc should not be modified
ND;

View File

@@ -1,16 +0,0 @@
<?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.
*/
class Pearlike_WithComments
{
/** @Boolean */
public static $loaded = true;
}

View File

@@ -1,36 +0,0 @@
<?php
trait TD
{}
trait TZ
{
use TD;
}
trait TC
{
use TD;
}
trait TB
{
use TC;
}
trait TA
{
use TB;
}
class CTFoo
{
use TA;
use TZ;
}
class CTBar
{
use TZ;
use TA;
}

View File

@@ -23,7 +23,7 @@ class UniversalClassLoaderTest extends \PHPUnit_Framework_TestCase
$loader = new UniversalClassLoader();
$loader->registerNamespace('Namespaced', __DIR__.DIRECTORY_SEPARATOR.'Fixtures');
$loader->registerPrefix('Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures');
$this->assertTrue($loader->loadClass($testClassName));
$loader->loadClass($testClassName);
$this->assertTrue(class_exists($className), $message);
}
@@ -92,7 +92,7 @@ class UniversalClassLoaderTest extends \PHPUnit_Framework_TestCase
$loader->registerPrefix('Pearlike_', __DIR__.DIRECTORY_SEPARATOR.'Fixtures');
$loader->registerNamespaceFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback'));
$loader->registerPrefixFallbacks(array(__DIR__.DIRECTORY_SEPARATOR.'Fixtures/fallback'));
$this->assertTrue($loader->loadClass($testClassName));
$loader->loadClass($testClassName);
$this->assertTrue(class_exists($className), $message);
}
@@ -128,7 +128,7 @@ class UniversalClassLoaderTest extends \PHPUnit_Framework_TestCase
$loader = new UniversalClassLoader();
$loader->registerNamespaces($namespaces);
$this->assertTrue($loader->loadClass($className));
$loader->loadClass($className);
$this->assertTrue(class_exists($className), $message);
}
@@ -178,7 +178,7 @@ class UniversalClassLoaderTest extends \PHPUnit_Framework_TestCase
$loader = new UniversalClassLoader();
$loader->registerPrefixes($prefixes);
$this->assertTrue($loader->loadClass($className));
$loader->loadClass($className);
$this->assertTrue(class_exists($className), $message);
}

22
Tests/bootstrap.php Normal file
View File

@@ -0,0 +1,22 @@
<?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.
*/
spl_autoload_register(function ($class) {
if (0 === strpos(ltrim($class, '/'), 'Symfony\Component\ClassLoader')) {
if (file_exists($file = __DIR__.'/../'.substr(str_replace('\\', '/', $class), strlen('Symfony\Component\ClassLoader')).'.php')) {
require_once $file;
}
}
});
if (file_exists($loader = __DIR__.'/../vendor/autoload.php')) {
require_once $loader;
}

View File

@@ -242,15 +242,11 @@ class UniversalClassLoader
* Loads the given class or interface.
*
* @param string $class The name of the class
*
* @return Boolean|null True, if loaded
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
require $file;
return true;
}
}

View File

@@ -51,9 +51,6 @@ class XcacheClassLoader
* @param string $prefix A prefix to create a namespace in Xcache
* @param object $classFinder An object that implements findFile() method.
*
* @throws \RuntimeException
* @throws \InvalidArgumentException
*
* @api
*/
public function __construct($prefix, $classFinder)

View File

@@ -20,15 +20,11 @@
"php": ">=5.3.3"
},
"require-dev": {
"symfony/finder": "~2.0"
"symfony/finder": "2.1.*"
},
"autoload": {
"psr-0": { "Symfony\\Component\\ClassLoader\\": "" }
"psr-0": { "Symfony\\Component\\ClassLoader": "" }
},
"target-dir": "Symfony/Component/ClassLoader",
"extra": {
"branch-alias": {
"dev-master": "2.2-dev"
}
}
"minimum-stability": "dev"
}

View File

@@ -9,7 +9,7 @@
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="vendor/autoload.php"
bootstrap="Tests/bootstrap.php"
>
<testsuites>
<testsuite name="Symfony ClassLoader Component Test Suite">