-
-
Save lyrixx/0adb8fd414451596557871d2d9af5695 to your computer and use it in GitHub Desktop.
<?php | |
namespace Tests\Integration; | |
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; | |
use Symfony\Component\Config\FileLocator; | |
use Symfony\Component\DependencyInjection\ContainerBuilder; | |
use Symfony\Component\DependencyInjection\Definition; | |
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; | |
class ContainerTest extends KernelTestCase | |
{ | |
private const FILTER_LIST = [ | |
// some services can exist only in dev or prod (thus not in test env) | |
// or some services are behind some features flags | |
// or some services are static (thus they are not real service) | |
]; | |
public function testContainer() | |
{ | |
static::bootKernel(['debug' => true]); | |
$projectDir = static::getContainer()->getParameter('kernel.project_dir'); | |
$container = static::getContainer(); | |
$builder = new ContainerBuilder(); | |
$loader = new XmlFileLoader($builder, new FileLocator()); | |
$loader->load($container->getParameter('debug.container.dump')); | |
$count = 0; | |
foreach ($builder->getDefinitions() as $id => $service) { | |
if ($this->isSkipped($id, $service, $builder, $projectDir)) { | |
continue; | |
} | |
$container->get($id); | |
++$count; | |
} | |
$this->addToAssertionCount($count); | |
} | |
private function isSkipped(string $id, Definition $service, ContainerBuilder $builder, string $projectDir): bool | |
{ | |
if (str_starts_with($id, '.instanceof.') || str_starts_with($id, '.abstract.') || str_starts_with($id, '.errored.')) { | |
return true; // Symfony internal stuff | |
} | |
if ($service->isAbstract()) { | |
return true; // Symfony internal stuff | |
} | |
$class = $service->getClass(); | |
if (!$class) { | |
return true; // kernel, or alias, or abstract | |
} | |
if (\in_array($class, self::FILTER_LIST)) { | |
return true; | |
} | |
$rc = $builder->getReflectionClass($class, false); | |
if (!$rc) { | |
return true; | |
} | |
$filename = $rc->getFileName(); | |
if (!str_starts_with($filename, "{$projectDir}/src")) { | |
return true; // service class not in tests/Integration | |
} | |
if ($rc->isAbstract()) { | |
return true; | |
} | |
return false; | |
} | |
} |
Very good idea. I'll update the gist asap
Gist updated ๐ Thanks again
In our project, we have a lot of service locators. I think the test should take these into account and unpack every ServiceLocator that it finds.
Another optimization that can be made: report all failures, instead of only the first one.
@lyrixx closing }
for the test class is missing in the gist ๐.
FYI, I ended up with something like this as a PoC:
class ContainerTest extends KernelTestCase
{
private const FILTER_LIST = [
// some services can exist only in dev or prod (thus not in test env)
// or some services are behind some features flags
// or some services are static (thus they are not real service)
];
public function testContainer(): void
{
static::bootKernel(['debug' => true]);
$projectDir = static::getContainer()->getParameter('kernel.project_dir');
$container = static::getContainer();
$this->bootstrapContainer($container);
$builder = new ContainerBuilder();
$loader = new XmlFileLoader($builder, new FileLocator());
$loader->load($container->getParameter('debug.container.dump'));
$invalidServices = [];
$count = 0;
foreach ($builder->getDefinitions() as $id => $service) {
if ($this->isSkipped($id, $service, $builder, $projectDir)) {
continue;
}
try {
$container->get($id);
} catch (\Throwable $e) {
$invalidServices[] = sprintf('[%s] %s', $id, $e->getMessage());
continue;
}
++$count;
}
$this->addToAssertionCount($count);
if (count($invalidServices) > 0) {
throw new \RuntimeException(sprintf(
"Invalid services found: \n - %s",
implode("\n\n - ", $invalidServices)
));
}
}
private function isSkipped(string $id, Definition $service, ContainerBuilder $builder, string $projectDir): bool
{
if (str_starts_with($id, '.instanceof.')
|| str_starts_with($id, '.abstract.')
|| str_starts_with($id, '.errored.')
|| $service->isAbstract() // Incomplete definitions that can't be instantiated
|| $service->isSynthetic() // Synthetic services don't actually have a definition, these are set in runtime
|| $service->isLazy() // @TODO support lazy services
) {
return true; // Symfony internal stuff
}
$class = $service->getClass();
if (!$class) {
return true; // kernel, or alias, or abstract
}
if (\in_array($class, self::FILTER_LIST)) {
return true;
}
$rc = $builder->getReflectionClass($class, false);
if (!$rc) {
return true;
}
$filename = $rc->getFileName();
if (false === $filename || str_starts_with($filename, "{$projectDir}/vendor/symfony")) {
return true;
}
if ($rc->isAbstract()) {
return true;
}
return false;
}
/**
* Do all the stuff required for bootstrapping services/factories, that are used when services are being built
*/
private function bootstrapContainer(ContainerInterface $container): void {
}
}
The differences from gist's version:
- iterate over all service, collect all the errors and print them at once at the end
- ignore synthetic services (these are meant to be set in runtime, no need to check them)
- ignore lazy services (it's something to tackle, I had some problems and did not want to spend much time on it)
- ignore Symfony services registered from
vendor/symfony
, instead of narrowing tosrc
only (our app has several dirs with production code, I did not want to list them all or to prepare complex logic) - skip if service reflection returned
false
Anyway, thanks for sharing, it was good to make some kind of experiment. What I found more than lint:container
is that we rely on EventDispatcher
, not on EventDispatcherInterface
, so when traceable event dispatcher is injected in test
environment, there's TypeError
, which means we should fix the signatures and use the interface as a contract.
PS. in our case bootstrapContainer()
has logic, I just removed it as it's crafted for our code. But if there are any things that need to be pre-defined before services are created (like $_SERVER['HTTP_CLIENT_IP']
for example), you also need to do it before iterating over container.
hey @lyrixx
thanks for this snippet.
I'm trying to implement this in one huge project, and I'm facing some memory limit problems. I tried to run with 'debug' => false
, or to shutdown kernel, but still have a huge memory usage with this test. Do you have some hints about this? thanks!
On project I'm working ATM:
- 6557 services in total
-
so 1008 services testedOK (1 test, 1008 assertions) Time: 00:01.455, Memory: 119.00 MB
So it looks like it does not consume too much memory
Maybe you have a memory leak somewhere...
we have more than 20.000 services and 4000 services tested, memory consumption ends at ~600 Mo just for this test ๐
Thanks for sharing this.
You can use this to automatically find the XML file:
Another improvement would be to use the XmlFileLoader so that you don't have to manually parse and work with the XML structure: